From cf80d967d9655411c850908104270bdc42616bc1 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:15:15 +0000 Subject: [PATCH] GP-6902: Add 'Assemble' action for multi-line assembly. Add 'Copy Assembly Code'. Go to next line after patch. --- .../DebuggerDisassemblerPlugin.html | 9 +- .../AbstractTraceAssemblePatchAction.java | 46 ++ .../AbstractTracePatchInstructionAction.java | 51 +- ...rrentPlatformTraceAssemblePatchAction.java | 46 ++ .../DebuggerDisassemblerPlugin.java | 4 + ...FixedPlatformTraceAssemblePatchAction.java | 61 ++ .../FixedPlatformTraceDisassembleAction.java | 13 +- ...edPlatformTracePatchInstructionAction.java | 19 +- .../disassemble/TraceAssemblePatchDialog.java | 56 ++ .../TracePatchAssemblyCommand.java | 52 ++ .../disassemble/TracePatchDataAction.java | 1 + .../core/debug/stack/StackUnwinderTest.java | 3 +- .../pcode/emu/symz3/SymZ3EmulatorFactory.java | 6 +- .../ghidra/symz3/gui/Z3SummaryProvider.java | 62 +- .../RepairDisassemblyScript.java | 6 +- .../help/topics/AssemblerPlugin/Assembler.htm | 18 +- .../help/topics/ClipboardPlugin/Clipboard.htm | 98 +-- .../ClipboardPlugin/images/CopySpecial.png | Bin 11975 -> 17138 bytes .../cmd/disassemble/ReDisassembleCommand.java | 6 +- .../AbstractAssemblePatchDialog.java | 530 ++++++++++++++ .../core/assembler/AbstractPatchAction.java | 25 +- .../AbstractPatchAssemblyCommand.java | 151 ++++ .../core/assembler/AssemblePatchAction.java | 117 ++++ .../core/assembler/AssemblePatchDialog.java | 37 + .../core/assembler/AssemblerPlugin.java | 93 +++ .../core/assembler/AssemblyDualTextField.java | 658 ++---------------- .../core/assembler/PatchAssemblyCommand.java | 46 ++ .../core/assembler/PatchDataAction.java | 3 +- .../assembler/PatchInstructionAction.java | 110 +-- .../completion/AssemblyAutocompleter.java | 217 ++++++ .../AssemblyAutocompletionModel.java | 202 ++++++ .../completion/AssemblyCompletion.java | 97 +++ .../assembler/completion/ContextChanges.java | 51 ++ .../completion/ErrorAssemblyCompletion.java | 38 + .../InstructionAssemblyCompletion.java | 106 +++ .../SuggestionAssemblyCompletion.java | 30 + .../CodeBrowserClipboardProvider.java | 39 +- .../assembler/AssemblerPluginTestHelper.java | 14 +- .../AutocompletionCellRenderer.java | 12 +- .../autocomplete/AutocompletionEvent.java | 35 +- .../autocomplete/AutocompletionListener.java | 6 +- ...r.java => TextComponentAutocompleter.java} | 161 ++--- .../widgets/textfield/TextFieldLinker.java | 8 +- .../app/plugin/assembler/AssemblyBuffer.java | 186 ++++- .../plugin/assembler/GenericAssembler.java | 4 +- .../sleigh/sem/AssemblyPatternBlock.java | 17 +- .../symbol/AssemblyNumericTerminal.java | 3 +- .../sleigh/tree/AssemblyParseSymbolToken.java | 26 + .../processors/sleigh/ContextCache.java | 7 +- .../program/disassemble/ReDisassembler.java | 32 +- .../ClipboardPluginScreenShots.java | 8 +- .../disassemble/ReDisassemblerTest.java | 2 +- 52 files changed, 2700 insertions(+), 928 deletions(-) create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTraceAssemblePatchAction.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceAssemblePatchAction.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceAssemblePatchAction.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TraceAssemblePatchDialog.java create mode 100644 Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchAssemblyCommand.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractAssemblePatchDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAssemblyCommand.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchAction.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchAssemblyCommand.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompleter.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompletionModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyCompletion.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ContextChanges.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ErrorAssemblyCompletion.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/InstructionAssemblyCompletion.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/SuggestionAssemblyCompletion.java rename Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/{TextFieldAutocompleter.java => TextComponentAutocompleter.java} (85%) create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/tree/AssemblyParseSymbolToken.java diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerDisassemblerPlugin/DebuggerDisassemblerPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerDisassemblerPlugin/DebuggerDisassemblerPlugin.html index 9560836251..e68f7210d2 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerDisassemblerPlugin/DebuggerDisassemblerPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerDisassemblerPlugin/DebuggerDisassemblerPlugin.html @@ -27,7 +27,14 @@ Other languages may also be presented if the only thing that varies is the default disassembly context, e.g., configuring ARM will also allow THUMB disassembly.

-

Patch Instruction / Assemble

+

Assemble

+ +

This action assembles a block of instructions starting at the cursor. One action is + presented per language configured at the current location. Other languages may also be + presented if the only thing that varies is the default disassembly context, e.g., configuring + ARM will also allow THUMB assembly.

+ +

Patch Instruction

This action assembles an instruction at the cursor. One action is presented per language configured at the current location. Other languages may also be presented if the only thing diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTraceAssemblePatchAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTraceAssemblePatchAction.java new file mode 100644 index 0000000000..1c8b7b0ea0 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTraceAssemblePatchAction.java @@ -0,0 +1,46 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.disassemble; + +import ghidra.app.context.ListingActionContext; +import ghidra.app.plugin.core.assembler.AbstractAssemblePatchDialog; +import ghidra.app.plugin.core.assembler.AssemblePatchAction; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Instruction; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.program.TraceProgramView; + +public abstract class AbstractTraceAssemblePatchAction extends AssemblePatchAction { + + public AbstractTraceAssemblePatchAction(DebuggerDisassemblerPlugin plugin, String name) { + super(plugin, name); + } + + protected abstract TracePlatform getPlatform(TraceProgramView view, Address entry); + + @Override + protected AbstractAssemblePatchDialog newDialog(ListingActionContext ctx) { + if (!(ctx.getProgram() instanceof TraceProgramView view)) { + return null; + } + Address entry = ctx.getAddress(); + Instruction ins = view.getListing().getInstructionContaining(entry); + RegisterValue initialContext = getInitialContext(ins, view, entry); + return new TraceAssemblePatchDialog(plugin.getTool(), ctx.getNavigatable(), + getPlatform(view, entry), view, entry, initialContext); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTracePatchInstructionAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTracePatchInstructionAction.java index e148abbb30..8d587051c0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTracePatchInstructionAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/AbstractTracePatchInstructionAction.java @@ -37,6 +37,31 @@ import ghidra.util.Msg; import ghidra.util.task.TaskMonitor; public abstract class AbstractTracePatchInstructionAction extends PatchInstructionAction { + protected class TraceAssemblyDualTextField extends AssemblyDualTextField { + protected class TraceAssemblyDualTextAutocompletionModel + extends AssemblyDualTextAutocompletionModel { + AssemblyPatternBlock ctx = null; + + @Override + protected AssemblyPatternBlock getContext() { + return ctx; + } + + @Override + public void setAddress(Address address) { + super.setAddress(address); + RegisterValue rv = getContextValue(getCodeUnit()); + ctx = rv == null ? AssemblyPatternBlock.nop() + : AssemblyPatternBlock.fromRegisterValue(rv).fillMask(); + } + } + + @Override + protected AssemblyDualTextAutocompletionModel newAutocompletionModel() { + return new TraceAssemblyDualTextAutocompletionModel(); + } + } + protected final DebuggerDisassemblerPlugin plugin; public AbstractTracePatchInstructionAction(DebuggerDisassemblerPlugin plugin, String name) { @@ -57,22 +82,7 @@ public abstract class AbstractTracePatchInstructionAction extends PatchInstructi @Override protected AssemblyDualTextField newAssemblyDualTextField() { - return new AssemblyDualTextField() { - AssemblyPatternBlock ctx = null; - - @Override - protected AssemblyPatternBlock getContext() { - return ctx; - } - - @Override - public void setAddress(Address address) { - super.setAddress(address); - RegisterValue rv = getContextValue(getCodeUnit()); - ctx = rv == null ? AssemblyPatternBlock.nop() - : AssemblyPatternBlock.fromRegisterValue(rv).fillMask(); - } - }; + return new TraceAssemblyDualTextField(); } @Override @@ -150,7 +160,14 @@ public abstract class AbstractTracePatchInstructionAction extends PatchInstructi } AddressSetView set = new AddressSet(address, address.add(data.length - 1)); - TraceDisassembleCommand dis = new TraceDisassembleCommand(platform, address, set); + TraceDisassembleCommand dis = new TraceDisassembleCommand(platform, address, set) { + @Override + public boolean applyTo(TraceProgramView view, TaskMonitor monitor) { + boolean result = super.applyTo(view, monitor); + doGoToNext(data.length); + return result; + } + }; if (contextValue != null) { dis.setInitialContext(contextValue); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceAssemblePatchAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceAssemblePatchAction.java new file mode 100644 index 0000000000..6867e89f95 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/CurrentPlatformTraceAssemblePatchAction.java @@ -0,0 +1,46 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.disassemble; + +import docking.ActionContext; +import ghidra.app.context.ListingActionContext; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Instruction; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.listing.TraceInstruction; +import ghidra.trace.model.program.TraceProgramView; + +public class CurrentPlatformTraceAssemblePatchAction extends AbstractTraceAssemblePatchAction { + + public CurrentPlatformTraceAssemblePatchAction(DebuggerDisassemblerPlugin plugin) { + super(plugin, "Assemble"); + } + + @Override + protected boolean isApplicableToContext(ActionContext context) { + return super.isApplicableToContext(context) && + context instanceof ListingActionContext lac && lac.getNavigatable().isDynamic(); + } + + @Override + protected TracePlatform getPlatform(TraceProgramView view, Address entry) { + Instruction ins = view.getListing().getInstructionContaining(entry); + if (!(ins instanceof TraceInstruction tins)) { // includes null + return view.getTrace().getPlatformManager().getHostPlatform(); + } + return tins.getPlatform(); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java index e3f04a5a2a..061b1b4532 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/DebuggerDisassemblerPlugin.java @@ -171,6 +171,7 @@ public class DebuggerDisassemblerPlugin extends Plugin implements PopupActionPro CurrentPlatformTraceDisassembleAction actionDisassemble; CurrentPlatformTracePatchInstructionAction actionPatchInstruction; TracePatchDataAction actionPatchData; + CurrentPlatformTraceAssemblePatchAction actionAssemblePatch; public DebuggerDisassemblerPlugin(PluginTool tool) { super(tool); @@ -187,10 +188,12 @@ public class DebuggerDisassemblerPlugin extends Plugin implements PopupActionPro actionDisassemble = new CurrentPlatformTraceDisassembleAction(this); actionPatchInstruction = new CurrentPlatformTracePatchInstructionAction(this); actionPatchData = new TracePatchDataAction(this); + actionAssemblePatch = new CurrentPlatformTraceAssemblePatchAction(this); tool.addAction(actionDisassemble); tool.addAction(actionPatchInstruction); tool.addAction(actionPatchData); + tool.addAction(actionAssemblePatch); } /** @@ -222,6 +225,7 @@ public class DebuggerDisassemblerPlugin extends Plugin implements PopupActionPro for (LanguageID langID : getAlternativeLanguageIDs(platform.getLanguage())) { result.add(new FixedPlatformTraceDisassembleAction(this, langID, platform)); result.add(new FixedPlatformTracePatchInstructionAction(this, langID, platform)); + result.add(new FixedPlatformTraceAssemblePatchAction(this, langID, platform)); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceAssemblePatchAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceAssemblePatchAction.java new file mode 100644 index 0000000000..4c1109dcf7 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceAssemblePatchAction.java @@ -0,0 +1,61 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.disassemble; + +import docking.action.MenuData; +import ghidra.app.plugin.core.assembler.AbstractPatchAction; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Program; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.listing.TraceInstruction; +import ghidra.trace.model.program.TraceProgramView; + +public class FixedPlatformTraceAssemblePatchAction extends AbstractTraceAssemblePatchAction { + + private final LanguageID altLangID; + private final TracePlatform platform; + + public FixedPlatformTraceAssemblePatchAction(DebuggerDisassemblerPlugin plugin, + LanguageID altLangID, TracePlatform platform) { + this.altLangID = altLangID; + this.platform = platform; + super(plugin, "Assemble using " + altLangID); + } + + @Override + protected MenuData createMenuData(String name) { + MenuData menuData = + new MenuData(new String[] { "Assemble using...", altLangID.toString() }); + menuData.setParentMenuGroup(AbstractPatchAction.MENU_GROUP); + return menuData; + } + + @Override + protected TracePlatform getPlatform(TraceProgramView view, Address entry) { + return platform; + } + + @Override + protected RegisterValue getInitialContext(Instruction ins, Program program, Address entry) { + Language language = ins instanceof TraceInstruction tins + ? tins.getLanguage() + : program.getLanguage(); + return DebuggerDisassemblerPlugin.deriveAlternativeDefaultContext(language, altLangID, + entry); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceDisassembleAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceDisassembleAction.java index bb5e2a531b..83a5e481f3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceDisassembleAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTraceDisassembleAction.java @@ -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. @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.debug.disassemble; import docking.action.MenuData; +import ghidra.app.plugin.core.assembler.AbstractPatchAction; import ghidra.program.model.lang.LanguageID; import ghidra.trace.model.guest.TracePlatform; import ghidra.trace.model.program.TraceProgramView; @@ -26,13 +27,13 @@ public class FixedPlatformTraceDisassembleAction extends AbstractTraceDisassembl public FixedPlatformTraceDisassembleAction(DebuggerDisassemblerPlugin plugin, LanguageID altLangID, TracePlatform platform) { - super(plugin, "Disassemble Trace as " + altLangID); this.altLangID = altLangID; this.platform = platform; + super(plugin, "Disassemble Trace as " + altLangID); - // TODO: Human-readable description? - setPopupMenuData( - new MenuData(new String[] { "Disassemble as " + altLangID }, "Disassembly")); + MenuData menuData = new MenuData(new String[] { "Disassemble as", altLangID.toString() }); + menuData.setParentMenuGroup(AbstractPatchAction.MENU_GROUP); + setPopupMenuData(menuData); } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTracePatchInstructionAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTracePatchInstructionAction.java index 135de2fd7f..687d95e352 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTracePatchInstructionAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/FixedPlatformTracePatchInstructionAction.java @@ -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. @@ -15,6 +15,8 @@ */ package ghidra.app.plugin.core.debug.disassemble; +import docking.action.MenuData; +import ghidra.app.plugin.core.assembler.AbstractPatchAction; import ghidra.program.model.lang.LanguageID; import ghidra.program.model.listing.CodeUnit; import ghidra.trace.model.guest.TracePlatform; @@ -25,11 +27,18 @@ public class FixedPlatformTracePatchInstructionAction extends AbstractTracePatch public FixedPlatformTracePatchInstructionAction(DebuggerDisassemblerPlugin plugin, LanguageID altLangID, TracePlatform platform) { - super(plugin, "Patch Instruction using " + altLangID); - setKeyBindingData(null); - this.altLangID = altLangID; this.platform = platform; + super(plugin, "Patch Instruction using " + altLangID); + setKeyBindingData(null); + } + + @Override + protected MenuData createMenuData(String name) { + MenuData menuData = + new MenuData(new String[] { "Patch Instruction using", altLangID.toString() }); + menuData.setParentMenuGroup(AbstractPatchAction.MENU_GROUP); + return menuData; } @Override diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TraceAssemblePatchDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TraceAssemblePatchDialog.java new file mode 100644 index 0000000000..ecbd1b5d03 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TraceAssemblePatchDialog.java @@ -0,0 +1,56 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.disassemble; + +import java.util.List; + +import ghidra.app.nav.Navigatable; +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.assembler.Assemblers; +import ghidra.app.plugin.core.assembler.AbstractAssemblePatchDialog; +import ghidra.app.plugin.core.assembler.AbstractPatchAssemblyCommand; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.program.TraceProgramView; + +public class TraceAssemblePatchDialog extends AbstractAssemblePatchDialog { + private final TracePlatform platform; + + protected TraceAssemblePatchDialog(PluginTool tool, Navigatable navigatable, + TracePlatform platform, TraceProgramView program, Address entry, + RegisterValue initialContext) { + this.platform = platform; + super(tool, navigatable, program, entry, initialContext); + } + + @Override + protected AbstractPatchAssemblyCommand newPatchCommand(List lines) { + return new TracePatchAssemblyCommand(platform, assembler, lines, entry, initialContext); + } + + @Override + protected Language getLanguage() { + return platform.getLanguage(); + } + + @Override + protected Assembler getAssembler() { + return Assemblers.getAssembler(getLanguage()); + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchAssemblyCommand.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchAssemblyCommand.java new file mode 100644 index 0000000000..33c5ee3e2a --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchAssemblyCommand.java @@ -0,0 +1,52 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.disassemble; + +import java.util.List; + +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.core.assembler.AbstractPatchAssemblyCommand; +import ghidra.framework.cmd.Command; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.model.guest.TracePlatform; +import ghidra.trace.model.program.TraceProgramView; + +public class TracePatchAssemblyCommand extends AbstractPatchAssemblyCommand { + + private final TracePlatform platform; + + public TracePatchAssemblyCommand(TracePlatform platform, Assembler asm, List lines, + Address entry, RegisterValue initialContext) { + this.platform = platform; + super(asm, lines, entry, initialContext); + } + + public TracePatchAssemblyCommand(TracePlatform platform, Assembler asm, String string, + Address entry, RegisterValue initialContext) { + this.platform = platform; + super(asm, string, entry, initialContext); + } + + @Override + protected Command newDisassembleCommand(AddressSetView set, + TraceProgramView program) { + TraceDisassembleCommand dis = new TraceDisassembleCommand(platform, entry, set); + dis.setInitialContext(initialContext); + return dis; + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchDataAction.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchDataAction.java index 682929b88f..cceeb22298 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchDataAction.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/disassemble/TracePatchDataAction.java @@ -62,6 +62,7 @@ public class TracePatchDataAction extends PatchDataAction { try { editor.setVariable(address, encoded).get(1, TimeUnit.SECONDS); + doGoToNext(encoded.length); // Let the trace do everything regarding existing units return true; } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java index 4a860f895d..43db79ecda 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/stack/StackUnwinderTest.java @@ -654,8 +654,7 @@ public class StackUnwinderTest extends AbstractGhidraHeadedDebuggerTest { Language language = asm.getLanguage(); Register regCtx = language.getContextBaseRegister(); Register regT = language.getRegister("T"); - RegisterValue rvDefault = new RegisterValue(regCtx, - asm.getContextAt(entry).toBigInteger(regCtx.getNumBytes())); + RegisterValue rvDefault = asm.getContextAt(entry).toRegisterValue(regCtx); RegisterValue rvThumb = rvDefault.assign(regT, BigInteger.ONE); AssemblyPatternBlock ctxThumb = AssemblyPatternBlock.fromRegisterValue(rvThumb); diff --git a/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/pcode/emu/symz3/SymZ3EmulatorFactory.java b/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/pcode/emu/symz3/SymZ3EmulatorFactory.java index e410508a41..0acb5cf0de 100644 --- a/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/pcode/emu/symz3/SymZ3EmulatorFactory.java +++ b/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/pcode/emu/symz3/SymZ3EmulatorFactory.java @@ -25,13 +25,13 @@ import ghidra.pcode.exec.trace.TraceEmulationIntegration.Writer; import ghidra.pcode.exec.trace.data.PcodeTraceAccess; /** - * An emulator factory for making the {@link SymZ3DebuggerPcodeEmulator} discoverable to the UI - * + * An emulator factory for making the {@link SymZ3PcodeEmulator} discoverable to the UI *

* This is the final class to create a full Debugger-integrated emulator. This class is what makes * it appear in the menu of possible emulators the user may configure. */ public class SymZ3EmulatorFactory implements EmulatorFactory { + public static final String TITLE = "Symbolic Z3 Summary with Concrete Emulation"; public static Writer delayedWriteTrace(PcodeTraceAccess access) { Writer writer = TraceEmulationIntegration.bytesDelayedWrite(access); @@ -45,7 +45,7 @@ public class SymZ3EmulatorFactory implements EmulatorFactory { @Override public String getTitle() { - return "Symbolic Z3 Summary with Concrete Emulation"; + return TITLE; } @Override diff --git a/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/symz3/gui/Z3SummaryProvider.java b/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/symz3/gui/Z3SummaryProvider.java index e946ab76a0..eeb937effe 100644 --- a/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/symz3/gui/Z3SummaryProvider.java +++ b/Ghidra/Extensions/SymbolicSummaryZ3/src/main/java/ghidra/symz3/gui/Z3SummaryProvider.java @@ -15,8 +15,10 @@ */ package ghidra.symz3.gui; -import java.awt.BorderLayout; +import java.awt.*; +import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import javax.swing.*; @@ -88,8 +90,9 @@ public class Z3SummaryProvider extends ComponentProviderAdapter { String style = ""; JPanel mainPanel = new JPanel(new BorderLayout()); - JSplitPane submainPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + JSplitPane summaryPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); JSplitPane codePanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + JPanel fixPanel = new JPanel(new GridBagLayout()); Z3SummaryInformationPanel information; Z3SummaryPcodeLogPanel ops; @@ -120,15 +123,30 @@ public class Z3SummaryProvider extends ComponentProviderAdapter { ops = new Z3SummaryPcodeLogPanel(this); instructions = new Z3SummaryInstructionLogPanel(this); - JPanel summaryPanel = new JPanel(new BorderLayout()); - summaryPanel.add(new JScrollPane(information)); codePanel.setTopComponent(instructions); codePanel.setBottomComponent(ops); codePanel.setDividerLocation(0.4); - submainPanel.setRightComponent(summaryPanel); - submainPanel.setLeftComponent(codePanel); - mainPanel.add(submainPanel); - setFactoryToZ3(); + summaryPanel.setRightComponent(instructions); + summaryPanel.setLeftComponent(codePanel); + mainPanel.add(summaryPanel); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.anchor = GridBagConstraints.NORTH; + fixPanel.add(new JLabel(""" + +

Z3 Emulator is not active

+

Press the button below to configure this tool for %s

""".formatted( + SymZ3EmulatorFactory.TITLE))); + JPanel buttons = new JPanel(new GridBagLayout()); + gbc.anchor = GridBagConstraints.CENTER; + gbc.fill = GridBagConstraints.HORIZONTAL; + + JButton buttonFix = new JButton("Fix"); + buttonFix.addActionListener(evt -> setFactoryToZ3()); + buttons.add(buttonFix); + gbc.weighty = 1; + fixPanel.add(buttons, gbc); } public void updateSummary() { @@ -148,8 +166,30 @@ public class Z3SummaryProvider extends ComponentProviderAdapter { PcodeMachine emu = emulationService.getCachedEmulator(trace, time); if (emu instanceof SymZ3PcodeEmulator z3Emu) { populateSummaryFromEmulator(z3Emu); + ensureMainPanel(summaryPanel); } + else { + clearSummary(); + ensureMainPanel(fixPanel); + } + } + private static boolean contains(Container container, Component component) { + int count = container.getComponentCount(); + for (int i = 0; i < count; i++) { + if (container.getComponent(i) == component) { + return true; + } + } + return false; + } + + private void ensureMainPanel(Component component) { + if (contains(mainPanel, component)) { + return; + } + mainPanel.removeAll(); + mainPanel.add(component); } private void setFactoryToZ3() { @@ -162,6 +202,12 @@ public class Z3SummaryProvider extends ComponentProviderAdapter { } } + public void clearSummary() { + information.setInformation(Stream.of(), Stream.of()); + ops.setLog(List.of()); + instructions.setLog(List.of()); + } + public void populateSummaryFromEmulator(SymZ3PcodeEmulator emu) { try (Context ctx = new Context()) { Z3InfixPrinter z3p = new Z3InfixPrinter(ctx); diff --git a/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java b/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java index 60dc43af41..66f03121fa 100644 --- a/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java @@ -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. @@ -20,6 +20,6 @@ public class RepairDisassemblyScript extends GhidraScript { @Override protected void run() throws Exception { ReDisassembler dis = new ReDisassembler(currentProgram); - dis.disasemble(currentAddress, monitor); + dis.disassemble(currentAddress, monitor); } } diff --git a/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm b/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm index 782ecf4628..64a94b8cbc 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/AssemblerPlugin/Assembler.htm @@ -19,7 +19,8 @@ ol.ratings li {

Assembler

-

+

Text fields in the listing for assembly code input

Assembly Editor

@@ -28,7 +29,7 @@ ol.ratings li {

Actions

-

There are two actions: One for instructions and one for data.

+

There are three actions: One for blocks, one for single instructions, and one for data.

Patch Instruction

@@ -93,6 +94,19 @@ ol.ratings li {

If the current processor scored anything less than Platinum you will receive a friendly warning reminding you what to expect.

+

Assemble...

+ +

This action is similar to Patch Instruction except that you can enter several instructions + in a sequence. See Copy + Assembly Code as a means of copying assembly passages from other parts of the program. The + dialog is fairly straightfoward: You enter lines of assembly code in the top text area, and the + table will display the tentative assembly result. The default columns are the Address and the + Assembly, which display the starting address and assembly of each instruction. There are + additional columns for diagnostics, including Bytes, Context, and Error. These display the + resulting machine code bytes, the input (dis)assembly context, and if applicable, any errors + encountered during assembly. When an error is encountered, the offending instruction's Assembly + cell is rendered in red, and no further instructions are assembled.

+

Patch Data

This action is available at initialized memory addresses having a data unit with a type that diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm b/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm index d23b41ecf7..caa37e8aac 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/Clipboard.htm @@ -29,7 +29,7 @@

  • Byte String
  • Byte String (no spaces)
  • - +
  • GhidraURL
  • @@ -69,9 +69,7 @@

    The clipboard supports the following operations:

    The Code Browser Listing window can paste the following formats:

    @@ -239,17 +245,15 @@
  • Byte String (No Spaces) - This is the same as the Byte String format, except there are no delimiting spaces between bytes.
  • - - -
  • Python Byte String - Copies the bytes into the Python byte string format, for - example: b'\x41\xec\x49\x89'.
  • - -
  • Python List String - Copies the bytes into the Python list format, for - example: [ 0x66, 0x2e, 0x0f, 0x1f, 0x84 ].
  • - -
  • C Array String - Copies the bytes into the C array format, for - example: { 0x66, 0x2e, 0x0f, 0x1f, 0x84 }.
  • - + +
  • Python Byte String - Copies the bytes into the Python byte string format, for + example: b'\x41\xec\x49\x89'.
  • + +
  • Python List String - Copies the bytes into the Python list format, for example: + [ 0x66, 0x2e, 0x0f, 0x1f, 0x84 ].
  • + +
  • C Array String - Copies the bytes into the C array format, for example: { + 0x66, 0x2e, 0x0f, 0x1f, 0x84 }.
  • The Byte Viewer Bytes In Memory window can paste the following formats:

    diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/images/CopySpecial.png b/Ghidra/Features/Base/src/main/help/help/topics/ClipboardPlugin/images/CopySpecial.png index 8cd9f185703bcd711041ce8b0bb0149d05626880..49b472b0ea9e2c24be288d4b88894200b3dc8b7c 100644 GIT binary patch literal 17138 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 vncOFgsmlOV7Nh@+9GsEZ { public boolean applyTo(Program program, TaskMonitor monitor) { ReDisassembler dis = new ReDisassembler(program); try { - dis.disasemble(seed, monitor); + dis.disassemble(seed, monitor); return true; } catch (CancelledException e) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractAssemblePatchDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractAssemblePatchDialog.java new file mode 100644 index 0000000000..0a960f7906 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractAssemblePatchDialog.java @@ -0,0 +1,530 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler; + +import java.awt.*; +import java.util.*; +import java.util.List; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.*; + +import db.Transaction; +import docking.DialogComponentProvider; +import docking.widgets.table.*; +import generic.theme.GThemeDefaults.Colors; +import generic.theme.GThemeDefaults.Colors.Tables; +import generic.theme.Gui; +import ghidra.app.nav.Navigatable; +import ghidra.app.plugin.assembler.*; +import ghidra.app.plugin.assembler.sleigh.grammars.AssemblySentential.WhiteSpaceParseToken; +import ghidra.app.plugin.assembler.sleigh.parse.*; +import ghidra.app.plugin.assembler.sleigh.sem.AssemblyPatternBlock; +import ghidra.app.plugin.assembler.sleigh.symbol.*; +import ghidra.app.plugin.assembler.sleigh.tree.*; +import ghidra.app.plugin.core.assembler.completion.*; +import ghidra.app.util.viewer.field.FieldFactory; +import ghidra.app.util.viewer.field.ListingColors; +import ghidra.app.util.viewer.field.ListingColors.LabelColors; +import ghidra.app.util.viewer.field.ListingColors.MnemonicColors; +import ghidra.async.AsyncDebouncer; +import ghidra.async.AsyncTimer; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.Language; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; + +public abstract class AbstractAssemblePatchDialog + extends DialogComponentProvider { + + protected static final Color COLOR_ERROR = Colors.ERROR; + protected static final Color COLOR_CONSTANT = ListingColors.CONSTANT; + protected static final Color COLOR_LABEL = LabelColors.PRIMARY; + protected static final Color COLOR_VARNODE = ListingColors.REGISTER; + protected static final Color COLOR_MNEMONIC = MnemonicColors.NORMAL; + protected static final Color COLOR_SEPARATOR = ListingColors.SEPARATOR; + + protected static final AttributeSet ATTRS_ERROR = makeAttributes(COLOR_ERROR, true); + protected static final AttributeSet ATTRS_CONSTANT = makeAttributes(COLOR_CONSTANT, false); + protected static final AttributeSet ATTRS_LABEL = makeAttributes(COLOR_LABEL, false); + protected static final AttributeSet ATTRS_VARNODE = makeAttributes(COLOR_VARNODE, false); + protected static final AttributeSet ATTRS_MNEMONIC = makeAttributes(COLOR_MNEMONIC, false); + protected static final AttributeSet ATTRS_SEPARATOR = makeAttributes(COLOR_SEPARATOR, false); + + static AttributeSet makeAttributes(Color color, boolean underline) { + MutableAttributeSet attributes = new SimpleAttributeSet(); + StyleConstants.setForeground(attributes, color); + StyleConstants.setUnderline(attributes, underline); + return attributes; + } + + record AssemblyRow(Address address, String assembly, AssemblyPatternBlock context, byte[] bytes, + Throwable error) {} + + class AsmTableModel extends GDynamicColumnTableModel { + final List modelData = new ArrayList<>(); + + public AsmTableModel(ServiceProvider serviceProvider) { + super(serviceProvider); + } + + @Override + public String getName() { + return "Assembly"; + } + + @Override + public List getModelData() { + return modelData; + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + return new TableColumnDescriptor<>() { + { + addHiddenColumn(new AsmContextColumn()); + addVisibleColumn(new AsmAddressColumn(), 1, true); + addHiddenColumn(new AsmBytesColumn()); + addVisibleColumn(new AsmAssemblyColumn()); + addHiddenColumn(new AsmErrorColumn()); + } + }; + } + + @Override + public Void getDataSource() { + return null; + } + + @Override + public boolean isSortable(int columnIndex) { + return false; + } + + public List getLines() { + return modelData.stream().map(AssemblyRow::assembly).toList(); + } + } + + static class MonospacedRenderer extends AbstractGColumnRenderer { + static final MonospacedRenderer INSTANCE = new MonospacedRenderer(); + + @SuppressWarnings("unchecked") + static GColumnRenderer instance() { + return (GColumnRenderer) INSTANCE; + } + + @Override + public String getFilterString(Object t, Settings settings) { + return t == null ? null : t.toString(); + } + + @Override + protected Font getDefaultFont() { + return fixedWidthFont; + } + } + + static abstract class AbstractAsmColumn + extends AbstractDynamicTableColumn { + @Override + public GColumnRenderer getColumnRenderer() { + return MonospacedRenderer.instance(); + } + } + + static class AsmAddressColumn extends AbstractAsmColumn
    { + @Override + public String getColumnName() { + return "Address"; + } + + @Override + public Address getValue(AssemblyRow row, Settings settings, Void data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row.address; + } + } + + static class AsmBytesColumn extends AbstractAsmColumn { + @Override + public String getColumnName() { + return "Bytes"; + } + + @Override + public String getValue(AssemblyRow row, Settings settings, Void data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row.bytes == null ? "" : NumericUtilities.convertBytesToString(row.bytes, " "); + } + } + + static class AsmContextColumn extends AbstractAsmColumn { + @Override + public String getColumnName() { + return "Context"; + } + + @Override + public String getValue(AssemblyRow row, Settings settings, Void data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row.context == null ? "" : row.context.toString(); + } + } + + static class AssemblyLineRenderer extends AbstractGColumnRenderer { + static final AssemblyLineRenderer INSTANCE = new AssemblyLineRenderer(); + private static final Color COLOR_ERROR = Tables.ERROR_UNSELECTED; + private static final Color COLOR_ERROR_SEL = Tables.ERROR_SELECTED; + + @Override + public String getFilterString(AssemblyRow t, Settings settings) { + return t == null ? null : t.assembly; + } + + @Override + protected String getText(Object value) { + if (!(value instanceof AssemblyRow row)) { + return Objects.toString(value); + } + return row.assembly; + } + + @Override + protected Font getDefaultFont() { + return fixedWidthFont; + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + Component ret = super.getTableCellRendererComponent(data); + if (!(data.getValue() instanceof AssemblyRow row)) { + return ret; + } + if (row.error != null) { + ret.setForeground(data.isSelected() ? COLOR_ERROR_SEL : COLOR_ERROR); + } + return ret; + } + } + + static class AsmErrorColumn extends AbstractAsmColumn { + @Override + public String getColumnName() { + return "Error"; + } + + @Override + public String getValue(AssemblyRow row, Settings settings, Void data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row.error == null ? "" : row.error.toString(); + } + } + + static class AsmAssemblyColumn + extends AbstractDynamicTableColumn { + @Override + public String getColumnName() { + return "Assembly"; + } + + @Override + public AssemblyRow getValue(AssemblyRow row, Settings settings, Void data, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return AssemblyLineRenderer.INSTANCE; + } + } + + protected final Navigatable navigatable; + protected final T program; + protected final Assembler assembler; + protected final Address entry; + protected final RegisterValue initialContext; + + protected final StyledDocument document; + protected final JTextPane text; + protected final AsmTableModel tableModel; + protected final GTable table; + + private final AsyncDebouncer update = new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, 500); + + protected AbstractAssemblePatchDialog(PluginTool tool, Navigatable navigatable, T program, + Address entry, RegisterValue initialContext) { + super("Assemble", true, true, true, true); + this.navigatable = navigatable; + this.program = program; + Language language = getLanguage(); + AssemblerPlugin.CACHE.get(language).get(null); + AssemblerPlugin.warnLanguage(language); + this.assembler = getAssembler(); + this.entry = entry; + this.initialContext = initialContext; + + update.addListener(_ -> reassemble()); + + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + + document = new DefaultStyledDocument(); + text = new JTextPane(document) { + { + Gui.registerFont(this, FieldFactory.BASE_LISTING_FONT_ID); + } + }; + document.addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + update.contact(null); + } + + @Override + public void removeUpdate(DocumentEvent e) { + update.contact(null); + } + + @Override + public void changedUpdate(DocumentEvent e) { + // Don't want to re-update on just attribute changes. + } + }); + JScrollPane spText = new JScrollPane(text); + spText.setPreferredSize(new Dimension(600, 600)); + split.setTopComponent(spText); + AssemblyAutocompletionModel autoModel = new AssemblyAutocompletionModel() { + @Override + protected void collectSuggestionsFromAccepted(Set result, + String text) { + Collection parses = assembler.parseLine(text); + for (AssemblyParseResult parse : parses) { + if (!parse.isError()) { + result.add(new SuggestionAssemblyCompletion("", + "%s".formatted(HTMLUtilities.escapeHTML(text)))); + return; + } + } + } + }; + autoModel.setAssembler(assembler); + autoModel.setAddress(entry); + autoModel.setExisting(program.getListing().getInstructionAt(entry)); + AssemblyAutocompleter auto = new AssemblyAutocompleter(autoModel) { + @Override + protected String getPrefix(JTextComponent field) { + String fullPrefix = super.getPrefix(field); + int whereLine = fullPrefix.lastIndexOf('\n'); + if (whereLine == -1) { + return fullPrefix; + } + return fullPrefix.substring(whereLine + 1); + } + + @Override + protected void createControlButtons(Box controls) { + // Remove the exhaust button + } + + @Override + protected Point getCompletionWindowPosition() { + Point caretPos = getCaretPositionOnScreen(text); + Point panePos = text.getLocationOnScreen(); + return new Point(panePos.x, caretPos.y); + } + }; + auto.attachTo(text); + + this.tableModel = new AsmTableModel(tool); + this.table = new GTable(tableModel); + split.setBottomComponent(new JScrollPane(table)); + split.setDividerLocation(0.5); + split.setResizeWeight(0.5); + + addWorkPanel(split); + addOKButton(); + addCancelButton(); + } + + protected Language getLanguage() { + return program.getLanguage(); + } + + protected Assembler getAssembler() { + return Assemblers.getAssembler(program); + } + + class Highlighter { + boolean seenWS; + int offset; + + Highlighter() { + document.setCharacterAttributes(0, document.getLength() + 1, SimpleAttributeSet.EMPTY, + true); + } + + void highlightToken(AssemblyParseToken token) { + AttributeSet attrs = switch (token) { + case AssemblyParseSymbolToken _ -> ATTRS_LABEL; + case WhiteSpaceParseToken _ -> { + seenWS = true; + yield ATTRS_SEPARATOR; + } + default -> switch (token.getSym()) { + case AssemblyNumericTerminal _ -> ATTRS_CONSTANT; + case AssemblyStringMapTerminal _ -> ATTRS_VARNODE; + case AssemblyStringTerminal st -> switch (st.getDefiningSymbol()) { + case null -> seenWS ? ATTRS_SEPARATOR : ATTRS_MNEMONIC; + default -> ATTRS_VARNODE; + }; + default -> ATTRS_SEPARATOR; + }; + }; + int length = token.getString().length(); + if (length != 0) { + document.setCharacterAttributes(offset, length, attrs, true); + } + offset += length; + } + + void highlightBranch(AssemblyParseBranch branch) { + for (AssemblyParseTreeNode node : branch.getSubstitutions()) { + switch (node) { + case AssemblyParseBranch sub -> highlightBranch(sub); + case AssemblyParseToken tok -> highlightToken(tok); + default -> { + } + } + } + } + + void highlightAccept(AssemblyParseAcceptResult accept) { + highlightBranch(accept.getTree()); + } + + void highlightError(String line, AssemblyParseErrorResult error) { + int length = error.getBuffer().length(); + int off = offset + line.length() - length; + document.setCharacterAttributes(off, length, ATTRS_ERROR, true); + } + + void highlight(String line) { + try { + Collection allResults = assembler.parseLine(line); + if (allResults.isEmpty()) { + return; + } + AssemblyParseResult correct = + allResults.stream().filter(r -> !r.isError()).findFirst().orElse(null); + if (correct instanceof AssemblyParseAcceptResult accept) { + highlightAccept(accept); + return; + } + AssemblyParseErrorResult error = allResults.stream() + .filter(r -> r instanceof AssemblyParseErrorResult) + .map(r -> (AssemblyParseErrorResult) r) + // Favor the parse that got farthest + .sorted(Comparator.comparing(r -> r.getBuffer().length())) + .findFirst() + .orElse(null); + if (error != null) { + highlightError(line, error); + return; + } + } + catch (Throwable t) { + Msg.error(this, t.getMessage()); + // Let the actual assembly step handle user display + } + } + + void newLine() { + seenWS = false; + offset += 1; + } + } + + void reassemble() { + tableModel.modelData.clear(); + + AssemblyBuffer buf = new AssemblyBuffer(assembler, entry, initialContext); + Highlighter highlighter = new Highlighter(); + for (String line : text.getText().lines().toList()) { + if (line.isBlank()) { + continue; + } + Address address = buf.getNext(); + AssemblyPatternBlock ctx = buf.getNextCtx(); + Swing.runLater(() -> highlighter.highlight(line)); + try { + byte[] bytes = buf.assemble(line); + tableModel.modelData.add(new AssemblyRow(address, line, ctx, bytes, null)); + } + catch (Throwable t) { + tableModel.modelData.add(new AssemblyRow(address, line, ctx, null, t)); + break; + } + Swing.runLater(() -> highlighter.newLine()); + } + Swing.runLater(() -> tableModel.fireTableDataChanged()); + } + + protected abstract AbstractPatchAssemblyCommand newPatchCommand(List lines); + + @Override + protected void okCallback() { + AbstractPatchAssemblyCommand patchCmd = newPatchCommand(tableModel.getLines()); + executeProgressTask(new Task("Assemble", true, true, false) { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + try (Transaction _ = program.openTransaction(patchCmd.getName())) { + if (patchCmd.applyTo(program, monitor)) { + Swing.runLater(() -> close()); + } + else { + setStatusText(patchCmd.getStatusMsg(), MessageType.ERROR); + } + } + catch (Exception e) { + setStatusText(e.getMessage(), MessageType.ERROR); + } + AddressSetView set = patchCmd.getSet(); + if (set != null) { + Swing.runLater(() -> navigatable.setSelection(new ProgramSelection(set))); + } + Address next = patchCmd.getNext(); + if (next != null) { + Swing.runLater(() -> navigatable.goTo(new ProgramLocation(program, next))); + } + } + }, 500); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java index 61811e5a87..a49a459ae7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAction.java @@ -27,6 +27,7 @@ import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.support.FieldLocation; import ghidra.GhidraOptions; import ghidra.app.context.ListingActionContext; +import ghidra.app.nav.Navigatable; import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; import ghidra.app.util.viewer.field.ListingField; import ghidra.app.util.viewer.listingpanel.ListingModelAdapter; @@ -35,10 +36,12 @@ import ghidra.framework.options.ToolOptions; import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressOutOfBoundsException; import ghidra.program.model.listing.CodeUnit; import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; +import ghidra.util.Swing; /** * An abstract action for patching @@ -47,7 +50,7 @@ import ghidra.program.util.ProgramLocation; * This handles most of the field placement, but relies on quite a few callbacks. */ public abstract class AbstractPatchAction extends DockingAction { - protected static final String MENU_GROUP = "Disassembly"; + public static final String MENU_GROUP = "Disassembly"; protected final PluginTool tool; @@ -56,6 +59,7 @@ public abstract class AbstractPatchAction extends DockingAction { private FieldPanel fieldPanel; private ListingPanel listingPanel; + private Navigatable navigatable; private Program program; private Address address; private CodeUnit codeUnit; @@ -158,6 +162,13 @@ public abstract class AbstractPatchAction extends DockingAction { */ protected abstract void setInputFont(Font font); + /** + * {@return the navigatable that had focus when this action was performed} + */ + protected Navigatable getNavigatable() { + return navigatable; + } + /** * Get the program on which this action was invoked * @@ -380,6 +391,7 @@ public abstract class AbstractPatchAction extends DockingAction { ListingActionContext lac = (ListingActionContext) context; prepareLayout(lac); + navigatable = lac.getNavigatable(); ProgramLocation cur = lac.getLocation(); program = cur.getProgram(); address = cur.getAddress(); @@ -407,4 +419,15 @@ public abstract class AbstractPatchAction extends DockingAction { fieldLayoutManager.layoutContainer(fieldPanel); } + + protected void doGoToNext(int length) { + program.flushEvents(); // Or else the listing layout may not be updated yet + try { + Address next = getAddress().add(length); + Swing.runLater(() -> navigatable.goTo(new ProgramLocation(program, next))); + } + catch (AddressOutOfBoundsException e) { + // Just don't goto + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAssemblyCommand.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAssemblyCommand.java new file mode 100644 index 0000000000..2cdd8eb67e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AbstractPatchAssemblyCommand.java @@ -0,0 +1,151 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler; + +import java.io.IOException; +import java.util.List; + +import ghidra.app.plugin.assembler.*; +import ghidra.framework.cmd.BackgroundCommand; +import ghidra.framework.cmd.Command; +import ghidra.program.disassemble.ReDisassembler; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public abstract class AbstractPatchAssemblyCommand extends BackgroundCommand { + protected final Assembler asm; + protected final List lines; + protected final Address entry; + protected final RegisterValue initialContext; + + private AddressSetView set; + private Address next; + protected String status = "Assembling"; + + public AbstractPatchAssemblyCommand(Assembler asm, List lines, Address entry, + RegisterValue initialContext) { + this.asm = asm; + this.lines = lines; + this.entry = entry; + this.initialContext = initialContext; + } + + public AbstractPatchAssemblyCommand(Assembler asm, String string, Address entry, + RegisterValue initialContext) { + this(asm, string.lines().toList(), entry, initialContext); + } + + abstract protected Command newDisassembleCommand(AddressSetView set, T program); + + public AddressSetView assemble(T program, TaskMonitor monitor) + throws CancelledException, IOException, MemoryAccessException, AssemblyException { + monitor.setMessage("Constructing Assembler"); + monitor.checkCancelled(); + AssemblyBuffer buf = new AssemblyBuffer(asm, entry, initialContext); + + monitor.initialize(lines.size(), "Assembling"); + for (String line : lines) { + if (line.isBlank()) { + continue; + } + // LATER: Data directives? Label placement? + try { + buf.assemble(line); + monitor.increment(); + } + catch (AssemblySyntaxException | AssemblySemanticException e) { + throw new AssemblyException("Could not assemble: %s".formatted(line), e); + } + } + + monitor.initialize(0, "Placing bytes"); + monitor.checkCancelled(); + AddressSet set = new AddressSet(entry, buf.getNext().previous()); + // Get the command before we modify the memory/listing, so it can inspect things + Command dis = newDisassembleCommand(set, program); + + program.getListing() + .clearCodeUnits(set.getMinAddress(), set.getMaxAddress(), true, monitor); + program.getMemory().setBytes(entry, buf.getBytes()); + + monitor.setMessage("Disassembling"); + monitor.checkCancelled(); + + // Might not succeed... + dis.applyTo(program); + + monitor.setMessage("Repairing disassembly"); + monitor.checkCancelled(); + ReDisassembler redis = new ReDisassembler(program); + AddressSet seeds = new AddressSet(); + for (Instruction ins : program.getListing().getInstructions(set, true)) { + for (Address seed : ins.getFlows()) { + seeds.add(seed); + } + } + InstructionIterator lastIt = program.getListing().getInstructions(set, false); + Instruction last = lastIt.hasNext() ? lastIt.next() : null; + if (last != null && last.getFlowType().hasFallthrough()) { + Address fall = last.getMaxAddress().next(); + if (fall != null) { + seeds.add(fall); + } + } + redis.disassemble(seeds, monitor); + return set; + } + + @Override + public final boolean applyTo(T program, TaskMonitor monitor) { + try { + set = assemble(program, monitor); + next = set.getMaxAddress().next(); + return true; + } + catch (CancelledException e) { + return false; + } + catch (MemoryAccessException | IOException e) { + throw new AssertionError(); + } + catch (AssemblyException e) { + status = e.getMessage(); + return false; + } + } + + @Override + public String getStatusMsg() { + return status; + } + + @Override + public String getName() { + return "Assemble"; + } + + public AddressSetView getSet() { + return set; + } + + public Address getNext() { + return next; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchAction.java new file mode 100644 index 0000000000..101671556d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchAction.java @@ -0,0 +1,117 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler; + +import docking.ActionContext; +import docking.ComponentProvider; +import docking.action.*; +import ghidra.app.context.ListingActionContext; +import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.framework.plugintool.Plugin; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.util.HelpLocation; + +public class AssemblePatchAction extends DockingAction { + protected final Plugin plugin; + + public AssemblePatchAction(Plugin plugin, String name) { + super(name, plugin.getName()); + setPopupMenuData(createMenuData(name)); + setKeyBindingData(new KeyBindingData("ctrl shift J")); + setHelpLocation(new HelpLocation(plugin.getName(), "assemble_patch")); + this.plugin = plugin; + } + + protected MenuData createMenuData(String name) { + return new MenuData(new String[] { "Assemble..." }, AbstractPatchAction.MENU_GROUP); + } + + protected boolean isApplicableToContext(ActionContext context) { + if (!(context instanceof ListingActionContext lac)) { + return false; + } + + ComponentProvider provider = lac.getComponentProvider(); + if (!(provider instanceof CodeViewerProvider codeViewer)) { + return false; + } + + if (codeViewer.isReadOnly()) { + return false; + } + + Program program = lac.getProgram(); + if (program == null) { + return false; + } + + Address address = lac.getAddress(); + if (address == null) { + return false; + } + MemoryBlock block = program.getMemory().getBlock(address); + if (block == null || !block.isInitialized()) { + return false; + } + return true; + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + return super.isEnabledForContext(context) && isApplicableToContext(context); + } + + @Override + public boolean isAddToPopup(ActionContext context) { + return super.isAddToPopup(context) && isApplicableToContext(context); + } + + protected RegisterValue getInitialContext(Instruction ins, Program program, Address entry) { + if (ins != null) { + return ins.getRegisterValue(ins.getBaseContextRegister()); + } + ProgramContext context = program.getProgramContext(); + return context.getRegisterValue(context.getBaseContextRegister(), entry); + } + + /** + * {@return a dialog for assembly a patch or null if the context is unsuitable} + * + * @param ctx the context + */ + protected AbstractAssemblePatchDialog newDialog(ListingActionContext ctx) { + Program program = ctx.getProgram(); + Address entry = ctx.getAddress(); + Instruction ins = program.getListing().getInstructionContaining(entry); + return new AssemblePatchDialog(plugin.getTool(), ctx.getNavigatable(), program, entry, + getInitialContext(ins, program, entry)); + } + + @Override + public void actionPerformed(ActionContext context) { + if (!(context instanceof ListingActionContext ctx)) { + return; + } + AbstractAssemblePatchDialog dialog = newDialog(ctx); + if (dialog == null) { + return; + } + plugin.getTool().showDialog(dialog); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchDialog.java new file mode 100644 index 0000000000..8d225f174c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblePatchDialog.java @@ -0,0 +1,37 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler; + +import java.util.List; + +import ghidra.app.nav.Navigatable; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Program; + +public class AssemblePatchDialog extends AbstractAssemblePatchDialog { + + protected AssemblePatchDialog(PluginTool tool, Navigatable navigatable, Program program, + Address entry, RegisterValue initialContext) { + super(tool, navigatable, program, entry, initialContext); + } + + @Override + protected AbstractPatchAssemblyCommand newPatchCommand(List lines) { + return new PatchAssemblyCommand(assembler, lines, entry, initialContext); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java index 976403460f..54eae6bfb5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblerPlugin.java @@ -15,17 +15,28 @@ */ package ghidra.app.plugin.core.assembler; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.collections4.map.DefaultedMap; +import org.apache.commons.collections4.map.LazyMap; + import docking.ActionContext; import ghidra.app.CorePluginPackage; import ghidra.app.context.ListingActionContext; import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.plugin.assembler.Assembler; import ghidra.app.plugin.assembler.Assemblers; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.data.DataType; +import ghidra.program.model.lang.Language; import ghidra.program.model.mem.MemBuffer; +import ghidra.util.Msg; +import ghidra.util.task.CachingSwingWorker; +import ghidra.util.task.TaskMonitor; /** * A plugin for assembly @@ -54,14 +65,79 @@ import ghidra.program.model.mem.MemBuffer; public class AssemblerPlugin extends ProgramPlugin { public static final String ASSEMBLER_NAME = "Assembler"; + private static final String ASSEMBLY_RATING = "assemblyRating"; + private static final String ASSEMBLY_MESSAGE = "assemblyMessage"; + + /** + * Enumerated quality ratings and text to describe them. + */ + static enum AssemblyRating { + UNRATED("This processor has not been tested with the assembler." + + " The assembler will probably work on this language."), + POOR("This processor received a rating of POOR during testing." + + " We DO NOT recommend trying to assemble."), + BRONZE("This processor received a rating of BRONZE during testing." + + " A fair number of instructions may assemble, but we DO NOT recommend trying to" + + " assemble."), + SILVER("This processor received a rating of SILVER during testing." + + " Most instructions should work, but you will likely encounter a few errors."), + GOLD("This processor received a rating of GOLD during testing." + + " You should rarely encounter an error."), + PLATINUM("This processor received a rating of PLATINUM during testing."); + + final String message; + + private AssemblyRating(String message) { + this.message = message; + } + } + + // To build the assembler in the background if it takes a while + private static class AssemblerConstructorWorker extends CachingSwingWorker { + private Language language; + + public AssemblerConstructorWorker(Language language) { + super("Assemble", false); + this.language = language; + } + + @Override + protected Assembler runInBackground(TaskMonitor monitor) { + monitor.setMessage("Constructing assembler for " + language); + return Assemblers.getAssembler(language); + } + } + + public static final Map> CACHE = + LazyMap.lazyMap(new HashMap<>(), language -> new AssemblerConstructorWorker(language)); + + /*test*/ static final Map SHOWN_WARNING = + DefaultedMap.defaultedMap(new HashMap<>(), false); + /*test*/ PatchInstructionAction patchInstructionAction; /*test*/ PatchDataAction patchDataAction; + /*test*/ AssemblePatchAction assemblePatchAction; public AssemblerPlugin(PluginTool tool) { super(tool); createActions(); } + protected static void warnLanguage(Language language) { + AssemblyRating rating = AssemblyRating.valueOf( + language.getProperty(ASSEMBLY_RATING + ":" + language.getLanguageID(), + AssemblyRating.UNRATED.name())); + if (AssemblyRating.PLATINUM != rating) { + String message = language.getProperty(ASSEMBLY_MESSAGE + ":" + language.getLanguageID(), + rating.message); + if (!SHOWN_WARNING.get(language)) { + Msg.showWarn(AssemblerPlugin.class, null, "Assembler Rating", + "

    " + message + "

    "); + SHOWN_WARNING.put(language, true); + } + } + } + private void createActions() { // Debugger provides its own "Patch" actions patchInstructionAction = new PatchInstructionAction(this) { @@ -97,6 +173,23 @@ public class AssemblerPlugin extends ProgramPlugin { } }; tool.addAction(patchDataAction); + + assemblePatchAction = new AssemblePatchAction(this, "Assemble") { + @Override + public boolean isEnabledForContext(ActionContext context) { + return super.isEnabledForContext(context) && + context instanceof ListingActionContext lac && + !lac.getNavigatable().isDynamic(); + } + + @Override + public boolean isAddToPopup(ActionContext context) { + return super.isAddToPopup(context) && + context instanceof ListingActionContext lac && + !lac.getNavigatable().isDynamic(); + } + }; + tool.addAction(assemblePatchAction); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java index 6c4366e019..e188f26daa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/AssemblyDualTextField.java @@ -17,37 +17,31 @@ package ghidra.app.plugin.core.assembler; import java.awt.*; import java.awt.event.*; -import java.math.BigInteger; -import java.util.*; -import java.util.Map.Entry; +import java.util.Collection; import java.util.concurrent.atomic.AtomicReference; import javax.swing.*; import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; -import docking.EmptyBorderToggleButton; -import docking.widgets.autocomplete.*; +import docking.widgets.autocomplete.AutocompletionListener; +import docking.widgets.autocomplete.TextComponentAutocompleter; import docking.widgets.label.GDLabel; import docking.widgets.textfield.TextFieldLinker; -import generic.theme.*; -import generic.theme.GThemeDefaults.Colors; +import generic.theme.Gui; import ghidra.GhidraApplicationLayout; import ghidra.GhidraLaunchable; import ghidra.app.plugin.assembler.Assembler; import ghidra.app.plugin.assembler.Assemblers; -import ghidra.app.plugin.assembler.sleigh.parse.AssemblyParseErrorResult; -import ghidra.app.plugin.assembler.sleigh.parse.AssemblyParseResult; -import ghidra.app.plugin.assembler.sleigh.sem.*; -import ghidra.app.plugin.processors.sleigh.*; -import ghidra.app.util.viewer.field.ListingColors; +import ghidra.app.plugin.core.assembler.completion.*; +import ghidra.app.plugin.processors.sleigh.SleighLanguage; +import ghidra.app.plugin.processors.sleigh.SleighLanguageProvider; import ghidra.framework.Application; import ghidra.framework.ApplicationConfiguration; import ghidra.program.model.address.Address; -import ghidra.program.model.lang.*; -import ghidra.program.model.listing.*; -import ghidra.program.model.mem.ByteMemBufferImpl; -import ghidra.util.*; -import help.Help; +import ghidra.program.model.lang.LanguageID; +import ghidra.program.model.listing.Instruction; +import ghidra.util.NumericUtilities; /** * A pair of text fields suitable for guided assembly @@ -62,309 +56,58 @@ import help.Help; * *

    * To detect when the user has activated an instruction-bytes entry, add an - * {@link AutocompletionListener} and check that the selection is an {@link AssemblyInstruction}. - * Otherwise, the usual autocompletion behavior is applied automatically. + * {@link AutocompletionListener} and check that the selection is an + * {@link InstructionAssemblyCompletion}. Otherwise, the usual autocompletion behavior is applied + * automatically. */ public class AssemblyDualTextField { private static final String FONT_ID = "font.plugin.assembly.dual.text.field"; - private static final Color FG_PREFERENCE_MOST = - new GColor("color.fg.plugin.assembler.completion.most"); - private static final Color FG_PREFERENCE_MIDDLE = - new GColor("color.fg.plugin.assembler.completion.middle"); - private static final Color FG_PREFERENCE_LEAST = - new GColor("color.fg.plugin.assembler.completion.least"); - - protected final TextFieldLinker linker = new TextFieldLinker(); - protected final JTextField mnemonic = new JTextField(); - protected final JTextField operands = new JTextField(); - protected final JTextField assembly = new JTextField(); - - protected final AssemblyAutocompletionModel model = new AssemblyAutocompletionModel(); - protected final AssemblyAutocompleter auto = new AssemblyAutocompleter(model); - - protected Assembler assembler; - protected Address address; - protected Instruction existing; - protected boolean exhaustUndefined = false; /** - * A generic class for all items listed by the autocompleter - */ - public static class AssemblyCompletion implements Comparable { - private final String text; - private final String display; - private final Color color; - protected int order; - - public AssemblyCompletion(String text, String display, Color color, int order) { - this.text = text; - this.display = display; - this.color = color; - this.order = order; - } - - /** - * Get the foreground color for the item - * - * @return the color - */ - public Color getColor() { - return color; - } - - /** - * Get the (possibly HTML) text to display for the item - * - * @return the text - */ - public String getDisplay() { - return display; - } - - /** - * Get the text to insert when the item is activated - * - * @return the text - */ - public String getText() { - return text; - } - - /** - * Override this to permit activation by default, i.e., on CTRL-SPACE - * - * @return true to permit defaulting, false to prevent it - */ - public boolean getCanDefault() { - return false; - } - - @Override - public String toString() { - return getDisplay(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof AssemblyCompletion)) { - return false; - } - return this.toString().equals(o.toString()); - } - - @Override - public int hashCode() { - return this.toString().hashCode(); - } - - @Override - public int compareTo(AssemblyCompletion that) { - if (this.order != that.order) { - return this.order - that.order; - } - return this.toString().compareTo(that.toString()); - } - } - - /** - * Represents a textual suggestion to complete or partially complete an assembly instruction - */ - static class AssemblySuggestion extends AssemblyCompletion { - public AssemblySuggestion(String text, String display) { - super(text, display, null, 1); - } - - @Override - public boolean getCanDefault() { - return true; - } - } - - static class ContextChanges implements DisassemblerContextAdapter { - private final RegisterValue contextIn; - private final Map contextsOut = new TreeMap<>(); - - public ContextChanges(RegisterValue contextIn) { - this.contextIn = contextIn; - } - - @Override - public RegisterValue getRegisterValue(Register register) { - if (register.getBaseRegister() == contextIn.getRegister()) { - return contextIn.getRegisterValue(register); - } - return null; - } - - @Override - public void setFutureRegisterValue(Address address, RegisterValue value) { - RegisterValue current = contextsOut.get(address); - RegisterValue combined = current == null ? value : current.combineValues(value); - contextsOut.put(address, combined); - } - - public void addFlow(ProgramContext progCtx, Address after) { - contextsOut.put(after, progCtx.getFlowValue(contextIn)); - } - } - - /** - * Represents an encoding for a complete assembly instruction - * + * A listener which activates the autocompleter on ENTER (in addition to the default + * CTRL-SPACE). *

    - * These provide no insertion text, since their activation should be handled by a custom - * listener. + * Because the user must activate an entry to specify the desired assembly, we make ENTER pull + * up the list, hinting that the user must make a selection. */ - static class AssemblyInstruction extends AssemblyCompletion { - private final byte[] data; - private final ContextChanges contextChanges; - - public AssemblyInstruction(Program program, Language language, Address at, String text, - byte[] data, RegisterValue ctxVal, int preference) { - // TODO?: Description to display constructor tree information - super("", NumericUtilities.convertBytesToString(data, " "), - preference == 10000 ? FG_PREFERENCE_MOST - : preference == 5000 ? FG_PREFERENCE_MIDDLE : FG_PREFERENCE_LEAST, - -preference); - this.data = data; - this.contextChanges = new ContextChanges(ctxVal); - - try { - if (program != null) { - // Handle flow context first - contextChanges.addFlow(program.getProgramContext(), at.addWrap(data.length)); - // drop prototype, just want context changes (globalsets) - language.parse(new ByteMemBufferImpl(at, data, language.isBigEndian()), - contextChanges, false); - } + protected class EnterKeyListener implements KeyListener { + @Override + public void keyTyped(KeyEvent e) { + if (e.getKeyChar() != KeyEvent.VK_ESCAPE) { + auto.setCompletionListVisible(true); } - catch (InsufficientBytesException | UnknownInstructionException e) { - Msg.error(this, "Cannot disassembly just-assembled instruction?: " + - NumericUtilities.convertBytesToString(data)); - } - adjustOrderByContextChanges(program); } - private void adjustOrderByContextChanges(Program program) { - if (program == null) { + @Override + public void keyPressed(KeyEvent e) { + if (e.isConsumed()) { return; } - ProgramContext ctx = program.getProgramContext(); - Register ctxReg = ctx.getBaseContextRegister(); - for (Entry ent : contextChanges.contextsOut.entrySet()) { - RegisterValue defVal = ctx.getDefaultDisassemblyContext(); - RegisterValue newVal = defVal.combineValues(ent.getValue()); - RegisterValue curVal = - defVal.combineValues(ctx.getRegisterValue(ctxReg, ent.getKey())); - BigInteger changed = - newVal.getUnsignedValueIgnoreMask().xor(curVal.getUnsignedValueIgnoreMask()); - order += changed.bitCount(); + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (!auto.isCompletionListVisible()) { + auto.startCompletion((JTextField) e.getComponent()); + e.consume(); + } } } - /** - * Get the assembled instruction bytes - * - * @return the bytes - */ - public byte[] getData() { - return data; - } - @Override - public int compareTo(AssemblyCompletion ac) { - if (this.order != ac.order) { - return this.order - ac.order; - } - if (!(ac instanceof AssemblyInstruction)) { - return super.compareTo(ac); - } - AssemblyInstruction that = (AssemblyInstruction) ac; - if (this.data.length != that.data.length) { - return this.data.length - that.data.length; - } - return super.compareTo(ac); + public void keyReleased(KeyEvent e) { + // Blank } } - /** - * Represents the description of an error encountered during parsing or assembling - * - *

    - * NOTE: not used until error descriptions improve - */ - static class AssemblyError extends AssemblyCompletion { - private String text; - - public AssemblyError(String text, String desc) { - super(text, desc, Colors.ERROR, 1); - this.text = text; - } - - @Override - public String getText() { - return text; - } - } - - /** - * A model that just delegates to our completion function - */ - class AssemblyAutocompletionModel implements AutocompletionModel { - @Override - public Collection computeCompletions(String text) { - return AssemblyDualTextField.this.computeCompletions(text); - } - } - - /** - * A customized autocompleter for assembly - * - *

    - * This positions the list at the bottom left of the field(s), and considers the full text of - * the linked text boxes when retrieving the prefix. It also delegates the item styling to the - * item instances. - */ - class AssemblyAutocompleter extends TextFieldAutocompleter - implements AutocompletionListener { - public AssemblyAutocompleter(AutocompletionModel model) { + class MyAutocompleter extends AssemblyAutocompleter { + public MyAutocompleter() { super(model); } - void fakeFocusGained(JTextField field) { - listener.fakeFocusGained(field); - } - @Override - protected String getPrefix(JTextField field) { - if (field == assembly) { - return field.getText().substring(0, field.getCaretPosition()); + protected String getPrefix(JTextComponent component) { + if (component == assembly) { + return component.getText().substring(0, component.getCaretPosition()); } - return linker.getTextBeforeCursor(field); - } - - @Override - protected String getCompletionDisplay(AssemblyCompletion sel) { - return sel.getDisplay(); - } - - @Override - protected Color getCompletionForeground(AssemblyCompletion sel, boolean isSelected, - boolean cellHasFocus) { - if (!isSelected) { - return sel.getColor(); - } - return null; - } - - @Override - protected String getCompletionText(AssemblyCompletion sel) { - return sel.getText(); - } - - @Override - protected boolean getCompletionCanDefault(AssemblyCompletion sel) { - return sel.getCanDefault(); + return linker.getTextBeforeCursor((JTextField) component); } @Override @@ -397,189 +140,56 @@ public class AssemblyDualTextField { } return new Dimension(width, -1); } + } - private static final String CMD_EXHAUST = "Exhaust undefined bits"; - private static final String CMD_ZERO = "Zero undefined bits"; - - private JLabel hints; - private EmptyBorderToggleButton button; - + protected class AssemblyDualTextAutocompletionModel extends AssemblyAutocompletionModel { @Override - protected void addContent(JPanel content) { - JPanel panel = new JPanel(new BorderLayout()); - Box controls = Box.createHorizontalBox(); - Icon icon = new GIcon("icon.plugin.assembler.question"); - button = new EmptyBorderToggleButton(icon); - button.setToolTipText("Exhaust unspecified bits, otherwise zero them"); - button.addActionListener((e) -> { - exhaustUndefined = CMD_EXHAUST.equals(e.getActionCommand()); - if (exhaustUndefined) { - button.setActionCommand(CMD_ZERO); - } - else { - button.setActionCommand(CMD_EXHAUST); - } - auto.updateDisplayContents(); - }); - button.setActionCommand(CMD_EXHAUST); - controls.add(button); - panel.add(controls, BorderLayout.SOUTH); - hints = new JLabel(); - panel.add(hints); - content.add(panel, BorderLayout.SOUTH); - - addAutocompletionListener(this); - - } - - @Override - public void completionSelected(AutocompletionEvent ev) { - if (!(ev.getSelection() instanceof AssemblyInstruction ai)) { - hints.setText(""); - return; - } - - Program program = assembler.getProgram(); - if (program == null) { - hints.setText(""); - return; - } - - ProgramContext ctx = program.getProgramContext(); - Register ctxReg = ctx.getBaseContextRegister(); - StringBuilder sb = new StringBuilder(""" -

      - """.formatted(ListingColors.REGISTER.toHexString())); - boolean displayedAny = false; - for (Entry ent : ai.contextChanges.contextsOut.entrySet()) { - RegisterValue defVal = ctx.getDefaultDisassemblyContext(); - RegisterValue newVal = defVal.combineValues(ent.getValue()); - RegisterValue curVal = - defVal.combineValues(ctx.getRegisterValue(ctxReg, ent.getKey())); - - boolean displayedAddress = false; - for (Register sub : ctxReg.getChildRegisters()) { - BigInteger newSubVal = - newVal.getRegisterValue(sub).getUnsignedValueIgnoreMask(); - BigInteger curSubVal = - curVal.getRegisterValue(sub).getUnsignedValueIgnoreMask(); - if (Objects.equals(curSubVal, newSubVal)) { - continue; - } - if (!displayedAddress) { - sb.append(""" -
    • At %s
    • -
        - """.formatted(ent.getKey())); - displayedAddress = true; - } - sb.append(""" -
      • %s := 0x%s
      • - """.formatted(sub.getName(), newSubVal.toString(16))); - displayedAny = true; - } - if (displayedAddress) { - sb.append(""" -
      - """); - } - } - if (!displayedAny) { - hints.setText(""); - } - sb.append(""" -
    - """); - hints.setText(sb.toString()); - } - - @Override - public void completionActivated(AutocompletionEvent e) { + public Collection computeCompletions(String text) { + // Don't use text passed in. Get full text of dual fields. + return super.computeCompletions(getText()); } } - /** - * A listener which activates the autocompleter on ENTER (in addition to the default - * CTRL-SPACE). - * - *

    - * Because the user must activate an entry to specify the desired assembly, we make ENTER pull - * up the list, hinting that the user must make a selection. - */ - class EnterKeyListener implements KeyListener { - @Override - public void keyTyped(KeyEvent e) { - if (e.getKeyChar() != KeyEvent.VK_ESCAPE) { - auto.setCompletionListVisible(true); - } - } + protected final TextFieldLinker linker = new TextFieldLinker(); + protected final JTextField mnemonic = new JTextField(); + protected final JTextField operands = new JTextField(); + protected final JTextField assembly = new JTextField(); - @Override - public void keyPressed(KeyEvent e) { - if (e.isConsumed()) { - return; - } - if (e.getKeyCode() == KeyEvent.VK_ENTER) { - if (!auto.isCompletionListVisible()) { - auto.startCompletion((JTextField) e.getComponent()); - e.consume(); - } - } - } - - @Override - public void keyReleased(KeyEvent e) { - // Blank - } - } + protected final AssemblyAutocompletionModel model = newAutocompletionModel(); + protected final AssemblyAutocompleter auto = new MyAutocompleter(); + protected final EnterKeyListener enterListener = new EnterKeyListener(); /** * Construct the assembly text fields */ public AssemblyDualTextField() { - // Because the option must actually be selected from the list, cause the enter key to show - // the completions. - KeyListener kl = new EnterKeyListener(); // Configure the linked fields linker.linkField(mnemonic, "\\s+", " "); auto.attachTo(mnemonic); - mnemonic.addKeyListener(kl); + mnemonic.addKeyListener(enterListener); configureField(mnemonic); mnemonic.setName("AssemblerMnemonic"); linker.linkLastField(operands); auto.attachTo(operands); - operands.addKeyListener(kl); + operands.addKeyListener(enterListener); configureField(operands); operands.setName("AssemblerOperands"); operands.setFocusTraversalKeysEnabled(false); // Configure the unlinked variant auto.attachTo(assembly); - assembly.addKeyListener(kl); configureField(assembly); + assembly.addKeyListener(enterListener); assembly.setName("AssemblerSingleField"); assembly.setFocusTraversalKeysEnabled(false); } + protected AssemblyDualTextAutocompletionModel newAutocompletionModel() { + return new AssemblyDualTextAutocompletionModel(); + } + void dispose() { auto.dispose(); } @@ -590,12 +200,11 @@ public class AssemblyDualTextField { * @param assembler the assembler */ public void setAssembler(Assembler assembler) { - this.assembler = Objects.requireNonNull(assembler); + model.setAssembler(assembler); } /** * Set the address of the assembly instruction - * *

    * Note this will reset the existing instruction to null to prevent its accidental re-use. See * {@link #setExisting(Instruction)}. @@ -603,18 +212,16 @@ public class AssemblyDualTextField { * @param address the address */ public void setAddress(Address address) { - this.address = Objects.requireNonNull(address); - this.existing = null; + model.setAddress(address); } /** * Set the "existing" instruction used for ordering proposed instructions by "most similar" * - * @see #computePreference(AssemblyResolvedPatterns) * @param existing the existing instruction */ public void setExisting(Instruction existing) { - this.existing = existing; + model.setExisting(existing); } /** @@ -634,14 +241,14 @@ public class AssemblyDualTextField { public JTextField getOperandsField() { return operands; } - + /** * Get the button that toggles bit exhaustion * * @return the button */ public JButton getExhaustButton() { - return auto.button; + return auto.getExhaustButton(); } /** @@ -662,7 +269,7 @@ public class AssemblyDualTextField { * * @return the autocompleter */ - public TextFieldAutocompleter getAutocompleter() { + public TextComponentAutocompleter getAutocompleter() { return auto; } @@ -831,138 +438,6 @@ public class AssemblyDualTextField { Gui.registerFont(field, FONT_ID); } - /** - * Construct the HTML display for a given suggestion - * - *

    - * This is an extension point. - * - *

    - * Currently, this just shows the current prefix in bold, and the text that would be inserted as - * normal weight. - * - * @param prefix the text currently in the fields - * @param suggestion the text suggested by the assembly syntax analyzer - * @param bufferleft the portion of the prefix that is also part of the suggestion - * @return a formatted string that hints to the effect of selecting this suggestion - */ - protected String formatSuggestion(String prefix, String suggestion, String bufferleft) { - String extra = suggestion.substring(bufferleft.length()); - String before = prefix.substring(0, prefix.length() - bufferleft.length()); - return String.format("%s%s%s", before, bufferleft, extra); - } - - /** - * Provides an ordering for assembled instructions appearing in the list - * - *

    - * The items with the highest preference are positioned at the top of the list - * - *

    - * This is an extension point. - * - *

    - * Currently, a proposed instruction having the same constructor tree as the existing one is the - * most preferred. Second, are instructions having a similar tree as the existing one -- - * "similar" is not yet well defined, but at the moment, it means their constructor tree strings - * have a long common prefix. Third, instructions having the same encoded length as the existing - * one are preferred. Last, the shortest instructions are preferred. - * - * @param rc a resolved instruction - * @return a preference - */ - protected int computePreference(AssemblyResolvedPatterns rc) { - if (existing == null) { - return 0; - } - String myTree = rc.dumpConstructorTree(); - String exTree = - ((SleighInstructionPrototype) existing.getPrototype()).dumpConstructorTree(); - for (int i = 0; i < myTree.length(); i++) { - if (!myTree.startsWith(exTree.substring(0, i))) { - return rc.getInstructionLength() == existing.getLength() ? 5000 : i; - } - } - return 10000; - } - - /** - * Compute valid completions given the prefix - * - *

    - * This is an extension point. - * - *

    - * If text parses and assembles, then the completion set will include assembled instruction-byte - * entries. Note that there may still be valid textual completions to continue the instruction. - * The suggestions yielded by all syntax errors are used to create textual completions. If the - * suggestion is prefixed by the buffer where the syntax error occurred, then, the tail of that - * suggestion is made into a completion entry. - * - * @param text the prefix - * @return the collection of completion items - */ - protected Collection computeCompletions(String text) { - final AssemblyPatternBlock ctx = Objects.requireNonNull(getContext()); - - Set result = new TreeSet<>(); - Collection parses = assembler.parseLine(text); - for (AssemblyParseResult parse : parses) { - if (parse.isError()) { - AssemblyParseErrorResult err = (AssemblyParseErrorResult) parse; - String buffer = err.getBuffer(); - for (String s : err.getSuggestions()) { - if (s.startsWith(buffer)) { - result.add(new AssemblySuggestion(s.substring(buffer.length()), - formatSuggestion(text, s, buffer))); - } - } - } - } - - Program program = assembler.getProgram(); - Language language = assembler.getLanguage(); - Register ctxReg = language.getContextBaseRegister(); - RegisterValue ctxVal = new RegisterValue(ctxReg, ctx.toBigInteger(ctxReg.getNumBytes())); - // HACK (Sort of): Don't use text passed in. Get full text. - String fullText = getText(); - parses = assembler.parseLine(fullText); - for (AssemblyParseResult parse : parses) { - if (!parse.isError()) { - AssemblyResolutionResults sems = assembler.resolveTree(parse, address, ctx); - for (AssemblyResolution ar : sems) { - if (ar.isError()) { - //result.add(new AssemblyError("", ar.toString())); - continue; - } - AssemblyResolvedPatterns rc = (AssemblyResolvedPatterns) ar; - for (byte[] ins : rc.possibleInsVals(ctx)) { - AssemblyInstruction ai = new AssemblyInstruction(program, language, address, - text, Arrays.copyOf(ins, ins.length), ctxVal, computePreference(rc)); - result.add(ai); - if (!exhaustUndefined) { - break; - } - } - } - } - } - - if (result.isEmpty()) { - result.add(new AssemblyError("", "Invalid instruction and/or prefix")); - } - return result; - } - - /** - * Get the context for filtering completed instructions in the auto-completer - * - * @return the context - */ - protected AssemblyPatternBlock getContext() { - return assembler.getContextAt(address).fillMask(); - } - /** * A demonstration of the assembly GUI outside of Ghidra */ @@ -1005,8 +480,9 @@ public class AssemblyDualTextField { dialog.add(asm, BorderLayout.CENTER); input.getAutocompleter().addAutocompletionListener(e -> { - if (e.getSelection() instanceof AssemblyInstruction) { - AssemblyInstruction ins = (AssemblyInstruction) e.getSelection(); + if (e.getSelection() instanceof InstructionAssemblyCompletion) { + InstructionAssemblyCompletion ins = + (InstructionAssemblyCompletion) e.getSelection(); String data = NumericUtilities.convertBytesToString(ins.getData()); asm.setText(asm.getText() + data); input.clear(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchAssemblyCommand.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchAssemblyCommand.java new file mode 100644 index 0000000000..f89149936b --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchAssemblyCommand.java @@ -0,0 +1,46 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler; + +import java.util.List; + +import ghidra.app.cmd.disassemble.DisassembleCommand; +import ghidra.app.plugin.assembler.Assembler; +import ghidra.framework.cmd.Command; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Program; + +public class PatchAssemblyCommand extends AbstractPatchAssemblyCommand { + + public PatchAssemblyCommand(Assembler asm, List lines, Address entry, + RegisterValue initialContext) { + super(asm, lines, entry, initialContext); + } + + public PatchAssemblyCommand(Assembler asm, String string, Address entry, + RegisterValue initialContext) { + super(asm, string, entry, initialContext); + } + + @Override + protected Command newDisassembleCommand(AddressSetView set, Program program) { + DisassembleCommand dis = new DisassembleCommand(entry, set, false); + dis.setInitialContext(initialContext); + return dis; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java index c884e2a1e1..0cadd48575 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchDataAction.java @@ -121,6 +121,7 @@ public class PatchDataAction extends AbstractPatchAction { if (encoded.length != oldLength) { program.getListing().createData(address, dt, encoded.length); } + doGoToNext(encoded.length); } @Override @@ -143,7 +144,7 @@ public class PatchDataAction extends AbstractPatchAction { tool.setStatusInfo(e.getMessage(), true); return; } - try (Transaction tx = + try (Transaction _ = program.openTransaction("Patch Data @" + address + ": " + input.getText())) { applyPatch(rng, encoded); hide(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java index 9a364299ab..2d8f6a09be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/PatchInstructionAction.java @@ -18,15 +18,10 @@ package ghidra.app.plugin.core.assembler; import java.awt.Font; import java.awt.event.FocusListener; import java.awt.event.KeyListener; -import java.util.HashMap; -import java.util.Map; import javax.swing.BorderFactory; import javax.swing.KeyStroke; -import org.apache.commons.collections4.map.DefaultedMap; -import org.apache.commons.collections4.map.LazyMap; - import db.Transaction; import docking.action.KeyBindingData; import docking.action.MenuData; @@ -37,7 +32,9 @@ import generic.theme.GThemeDefaults.Colors; import ghidra.app.cmd.disassemble.ReDisassembleCommand; import ghidra.app.plugin.assembler.Assembler; import ghidra.app.plugin.assembler.Assemblers; -import ghidra.app.plugin.core.assembler.AssemblyDualTextField.*; +import ghidra.app.plugin.core.assembler.AssemblyDualTextField.VisibilityMode; +import ghidra.app.plugin.core.assembler.completion.AssemblyCompletion; +import ghidra.app.plugin.core.assembler.completion.InstructionAssemblyCompletion; import ghidra.framework.plugintool.Plugin; import ghidra.program.model.address.Address; import ghidra.program.model.lang.Language; @@ -45,7 +42,6 @@ import ghidra.program.model.listing.*; import ghidra.program.model.mem.MemoryAccessException; import ghidra.util.HelpLocation; import ghidra.util.Msg; -import ghidra.util.task.CachingSwingWorker; import ghidra.util.task.TaskMonitor; import help.Help; @@ -54,48 +50,6 @@ import help.Help; */ public class PatchInstructionAction extends AbstractPatchAction { - /** - * Enumerated quality ratings and text to describe them. - */ - static enum AssemblyRating { - UNRATED("This processor has not been tested with the assembler." + - " The assembler will probably work on this language."), - POOR("This processor received a rating of POOR during testing." + - " We DO NOT recommend trying to assemble."), - BRONZE("This processor received a rating of BRONZE during testing." + - " A fair number of instructions may assemble, but we DO NOT recommend trying to" + - " assemble."), - SILVER("This processor received a rating of SILVER during testing." + - " Most instructions should work, but you will likely encounter a few errors."), - GOLD("This processor received a rating of GOLD during testing." + - " You should rarely encounter an error."), - PLATINUM("This processor received a rating of PLATINUM during testing."); - - final String message; - - private AssemblyRating(String message) { - this.message = message; - } - } - - // To build the assembler in the background if it takes a while - private static class AssemblerConstructorWorker extends CachingSwingWorker { - private Language language; - - public AssemblerConstructorWorker(Language language) { - super("Assemble", false); - this.language = language; - } - - @Override - protected Assembler runInBackground(TaskMonitor monitor) { - monitor.setMessage("Constructing assembler for " + language); - return Assemblers.getAssembler(language); - } - } - - private static final String ASSEMBLY_RATING = "assemblyRating"; - private static final String ASSEMBLY_MESSAGE = "assemblyMessage"; private static final KeyStroke KEYBIND_PATCH_INSTRUCTION = KeyStroke.getKeyStroke("ctrl shift G"); @@ -110,19 +64,14 @@ public class PatchInstructionAction extends AbstractPatchAction { private class ListenerForAccept implements AutocompletionListener { @Override public void completionActivated(AutocompletionEvent ev) { - if (ev.getSelection() instanceof AssemblyInstruction) { - AssemblyInstruction ins = (AssemblyInstruction) ev.getSelection(); + if (ev.getSelection() instanceof InstructionAssemblyCompletion) { + InstructionAssemblyCompletion ins = + (InstructionAssemblyCompletion) ev.getSelection(); accept(ins); } } } - private final Map> cache = - LazyMap.lazyMap(new HashMap<>(), language -> new AssemblerConstructorWorker(language)); - - /*test*/ static final Map SHOWN_WARNING = - DefaultedMap.defaultedMap(new HashMap<>(), false); - /*test*/ final AssemblyDualTextField input = newAssemblyDualTextField(); private final ListenerForAccept listenerForAccept = new ListenerForAccept(); @@ -130,8 +79,8 @@ public class PatchInstructionAction extends AbstractPatchAction { protected Assembler assembler; // Callback to keep the autocompleter positioned under the fields - private FieldPanelOverLayoutListener listenerToMoveAutocompleter = ev -> { - TextFieldAutocompleter autocompleter = input.getAutocompleter(); + private FieldPanelOverLayoutListener listenerToMoveAutocompleter = _ -> { + TextComponentAutocompleter autocompleter = input.getAutocompleter(); if (autocompleter.isCompletionListVisible()) { autocompleter.updateDisplayLocation(); } @@ -144,7 +93,7 @@ public class PatchInstructionAction extends AbstractPatchAction { public PatchInstructionAction(Plugin owner, String name) { super(owner, name); - setPopupMenuData(new MenuData(new String[] { name }, MENU_GROUP)); + setPopupMenuData(createMenuData(name)); setKeyBindingData(new KeyBindingData(KEYBIND_PATCH_INSTRUCTION)); HelpLocation location = new HelpLocation(owner.getName(), "patch_instruction"); setHelpLocation(location); @@ -154,14 +103,18 @@ public class PatchInstructionAction extends AbstractPatchAction { input.getAssemblyField().setBorder(BorderFactory.createLineBorder(Colors.ERROR, 2)); input.getAutocompleter().addAutocompletionListener(listenerForAccept); - + Help.getHelpService().registerHelp(input.getMnemonicField(), location); Help.getHelpService().registerHelp(input.getOperandsField(), location); Help.getHelpService().registerHelp(input.getExhaustButton(), location); - + init(); } + protected MenuData createMenuData(String name) { + return new MenuData(new String[] { name }, MENU_GROUP); + } + protected AssemblyDualTextField newAssemblyDualTextField() { return new AssemblyDualTextField(); } @@ -197,21 +150,6 @@ public class PatchInstructionAction extends AbstractPatchAction { return true; } - protected void warnLanguage() { - AssemblyRating rating = AssemblyRating.valueOf( - language.getProperty(ASSEMBLY_RATING + ":" + language.getLanguageID(), - AssemblyRating.UNRATED.name())); - if (AssemblyRating.PLATINUM != rating) { - String message = language.getProperty(ASSEMBLY_MESSAGE + ":" + language.getLanguageID(), - rating.message); - if (!SHOWN_WARNING.get(language)) { - Msg.showWarn(this, null, "Assembler Rating", - "

    " + message + "

    "); - SHOWN_WARNING.put(language, true); - } - } - } - protected Language getLanguage(CodeUnit cu) { return cu.getProgram().getLanguage(); } @@ -224,8 +162,8 @@ public class PatchInstructionAction extends AbstractPatchAction { protected void prepare() { CodeUnit cu = getCodeUnit(); language = getLanguage(cu); - cache.get(language).get(null); - warnLanguage(); + AssemblerPlugin.CACHE.get(language).get(null); + AssemblerPlugin.warnLanguage(language); assembler = getAssembler(cu); } @@ -311,7 +249,15 @@ public class PatchInstructionAction extends AbstractPatchAction { // NB. This will immediately re-disassembly the one command. // We'll background the context repair, which may include more disassembly assembler.patchProgram(data, getAddress()); - ReDisassembleCommand cmd = new ReDisassembleCommand(getAddress()); + + ReDisassembleCommand cmd = new ReDisassembleCommand(getAddress()) { + @Override + public boolean applyTo(Program program, TaskMonitor monitor) { + boolean result = super.applyTo(program, monitor); + doGoToNext(data.length); + return result; + } + }; tool.executeBackgroundCommand(cmd, getProgram()); } @@ -320,10 +266,10 @@ public class PatchInstructionAction extends AbstractPatchAction { * * @param ins the selected instruction from the completion list */ - public void accept(AssemblyInstruction ins) { + public void accept(InstructionAssemblyCompletion ins) { Program program = getProgram(); Address address = getAddress(); - try (Transaction tx = + try (Transaction _ = program.openTransaction("Assemble @" + address + ": " + input.getText())) { applyPatch(ins.getData()); hide(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompleter.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompleter.java new file mode 100644 index 0000000000..78f2302144 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompleter.java @@ -0,0 +1,217 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler.completion; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.math.BigInteger; +import java.util.Map.Entry; +import java.util.Objects; + +import javax.swing.*; + +import docking.EmptyBorderToggleButton; +import docking.widgets.autocomplete.*; +import generic.theme.GColor; +import generic.theme.GIcon; +import ghidra.app.util.viewer.field.ListingColors; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.Register; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.ProgramContext; + +/** + * A customized autocompleter for assembly + * + *

    + * This positions the list at the bottom left of the field(s), and considers the full text of the + * linked text boxes when retrieving the prefix. It also delegates the item styling to the item + * instances. + */ +public class AssemblyAutocompleter extends TextComponentAutocompleter + implements AutocompletionListener { + static final Color FG_PREFERENCE_MOST = + new GColor("color.fg.plugin.assembler.completion.most"); + static final Color FG_PREFERENCE_MIDDLE = + new GColor("color.fg.plugin.assembler.completion.middle"); + static final Color FG_PREFERENCE_LEAST = + new GColor("color.fg.plugin.assembler.completion.least"); + + protected boolean exhaustUndefined = false; + private final AssemblyAutocompletionModel model; + + public AssemblyAutocompleter(AssemblyAutocompletionModel model) { + super(model); + this.model = model; + } + + /*testing*/ public void fakeFocusGained(JTextField field) { + listener.fakeFocusGained(field); + } + + @Override + protected String getCompletionDisplay(AssemblyCompletion sel) { + return sel.getDisplay(); + } + + @Override + protected Color getCompletionForeground(AssemblyCompletion sel, boolean isSelected, + boolean cellHasFocus) { + if (!isSelected) { + return sel.getColor(); + } + return null; + } + + @Override + protected String getCompletionText(AssemblyCompletion sel) { + return sel.getText(); + } + + @Override + protected boolean getCompletionCanDefault(AssemblyCompletion sel) { + return sel.getCanDefault(); + } + + private static final String CMD_EXHAUST = "Exhaust undefined bits"; + private static final String CMD_ZERO = "Zero undefined bits"; + + private JLabel hints; + EmptyBorderToggleButton exhaustButton; + + protected void createControlButtons(Box controls) { + Icon icon = new GIcon("icon.plugin.assembler.question"); + exhaustButton = new EmptyBorderToggleButton(icon); + exhaustButton.setToolTipText("Exhaust unspecified bits, otherwise zero them"); + exhaustButton.addActionListener((e) -> { + exhaustUndefined = CMD_EXHAUST.equals(e.getActionCommand()); + if (exhaustUndefined) { + exhaustButton.setActionCommand(CMD_ZERO); + } + else { + exhaustButton.setActionCommand(CMD_EXHAUST); + } + updateDisplayContents(); + }); + exhaustButton.setActionCommand(CMD_EXHAUST); + controls.add(exhaustButton); + } + + @Override + protected void addContent(JPanel content) { + JPanel panel = new JPanel(new BorderLayout()); + Box controls = Box.createHorizontalBox(); + createControlButtons(controls); + panel.add(controls, BorderLayout.SOUTH); + hints = new JLabel(); + panel.add(hints); + content.add(panel, BorderLayout.SOUTH); + + addAutocompletionListener(this); + + } + + protected Program getProgram() { + return model.getAssembler().getProgram(); + } + + @Override + public void completionSelected(AutocompletionEvent ev) { + if (!(ev.getSelection() instanceof InstructionAssemblyCompletion ai)) { + hints.setText(""); + return; + } + + Program program = getProgram(); + if (program == null) { + hints.setText(""); + return; + } + + ProgramContext ctx = program.getProgramContext(); + Register ctxReg = ctx.getBaseContextRegister(); + StringBuilder sb = new StringBuilder(""" +

      + """.formatted(ListingColors.REGISTER.toHexString())); + boolean displayedAny = false; + for (Entry ent : ai.contextChanges.contextsOut.entrySet()) { + RegisterValue defVal = ctx.getDefaultDisassemblyContext(); + RegisterValue newVal = defVal.combineValues(ent.getValue()); + RegisterValue curVal = + defVal.combineValues(ctx.getRegisterValue(ctxReg, ent.getKey())); + + boolean displayedAddress = false; + for (Register sub : ctxReg.getChildRegisters()) { + BigInteger newSubVal = + newVal.getRegisterValue(sub).getUnsignedValueIgnoreMask(); + BigInteger curSubVal = + curVal.getRegisterValue(sub).getUnsignedValueIgnoreMask(); + if (Objects.equals(curSubVal, newSubVal)) { + continue; + } + if (!displayedAddress) { + sb.append(""" +
    • At %s
    • +
        + """.formatted(ent.getKey())); + displayedAddress = true; + } + sb.append(""" +
      • %s := 0x%s
      • + """.formatted(sub.getName(), newSubVal.toString(16))); + displayedAny = true; + } + if (displayedAddress) { + sb.append(""" +
      + """); + } + } + if (!displayedAny) { + hints.setText(""); + } + sb.append(""" +
    + """); + hints.setText(sb.toString()); + } + + @Override + public void completionActivated(AutocompletionEvent e) { + } + + public EmptyBorderToggleButton getExhaustButton() { + return exhaustButton; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompletionModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompletionModel.java new file mode 100644 index 0000000000..c4a310cabd --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyAutocompletionModel.java @@ -0,0 +1,202 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler.completion; + +import java.util.*; + +import docking.widgets.autocomplete.AutocompletionModel; +import ghidra.app.plugin.assembler.Assembler; +import ghidra.app.plugin.assembler.sleigh.parse.AssemblyParseErrorResult; +import ghidra.app.plugin.assembler.sleigh.parse.AssemblyParseResult; +import ghidra.app.plugin.assembler.sleigh.sem.*; +import ghidra.app.plugin.processors.sleigh.SleighInstructionPrototype; +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Program; +import ghidra.util.HTMLUtilities; + +public class AssemblyAutocompletionModel implements AutocompletionModel { + + protected Assembler assembler; + protected Address address; + protected Instruction existing; + protected boolean exhaustUndefined; + + /** + * Set the assembler to use + * + * @param assembler the assembler + */ + public void setAssembler(Assembler assembler) { + this.assembler = Objects.requireNonNull(assembler); + } + + /** + * {@return the assembler used by this model} + */ + public Assembler getAssembler() { + return assembler; + } + + /** + * Set the address of the assembly instruction + *

    + * Note this will reset the existing instruction to null to prevent its accidental re-use. See + * {@link #setExisting(Instruction)}. + * + * @param address the address + */ + public void setAddress(Address address) { + this.address = Objects.requireNonNull(address); + this.existing = null; + } + + /** + * Set the "existing" instruction used for ordering proposed instructions by "most similar" + * + * @see #computePreference(AssemblyResolvedPatterns) + * @param existing the existing instruction + */ + public void setExisting(Instruction existing) { + this.existing = existing; + } + + /** + * Construct the HTML display for a given suggestion + * + *

    + * This is an extension point. + * + *

    + * Currently, this just shows the current prefix in bold, and the text that would be inserted as + * normal weight. + * + * @param prefix the text currently in the fields + * @param suggestion the text suggested by the assembly syntax analyzer + * @param bufferleft the portion of the prefix that is also part of the suggestion + * @return a formatted string that hints to the effect of selecting this suggestion + */ + protected String formatSuggestion(String prefix, String suggestion, String bufferleft) { + String extra = suggestion.substring(bufferleft.length()); + String before = prefix.substring(0, prefix.length() - bufferleft.length()); + return String.format("%s%s%s", HTMLUtilities.escapeHTML(before), + HTMLUtilities.escapeHTML(bufferleft), HTMLUtilities.escapeHTML(extra)); + } + + /** + * Provides an ordering for assembled instructions appearing in the list + * + *

    + * The items with the highest preference are positioned at the top of the list + * + *

    + * This is an extension point. + * + *

    + * Currently, a proposed instruction having the same constructor tree as the existing one is the + * most preferred. Second, are instructions having a similar tree as the existing one -- + * "similar" is not yet well defined, but at the moment, it means their constructor tree strings + * have a long common prefix. Third, instructions having the same encoded length as the existing + * one are preferred. Last, the shortest instructions are preferred. + * + * @param rc a resolved instruction + * @return a preference + */ + protected int computePreference(AssemblyResolvedPatterns rc) { + if (existing == null) { + return 0; + } + String myTree = rc.dumpConstructorTree(); + String exTree = + ((SleighInstructionPrototype) existing.getPrototype()).dumpConstructorTree(); + for (int i = 0; i < myTree.length(); i++) { + if (!myTree.startsWith(exTree.substring(0, i))) { + return rc.getInstructionLength() == existing.getLength() ? 5000 : i; + } + } + return 10000; + } + + /** + * Get the context for filtering completed instructions in the auto-completer + * + * @return the context + */ + protected AssemblyPatternBlock getContext() { + return assembler.getContextAt(address).fillMask(); + } + + protected void collectSuggestionsFromErrors(Set result, String text) { + Collection parses = assembler.parseLine(text); + for (AssemblyParseResult parse : parses) { + if (parse.isError()) { + AssemblyParseErrorResult err = (AssemblyParseErrorResult) parse; + String buffer = err.getBuffer(); + for (String s : err.getSuggestions()) { + if (s.startsWith(buffer)) { + result.add(new SuggestionAssemblyCompletion(s.substring(buffer.length()), + formatSuggestion(text, s, buffer))); + } + } + } + } + } + + protected void collectSuggestionsFromAccepted(Set result, String text) { + final AssemblyPatternBlock ctx = Objects.requireNonNull(getContext()); + Program program = assembler.getProgram(); + Language language = assembler.getLanguage(); + Register ctxReg = language.getContextBaseRegister(); + RegisterValue ctxVal = ctx.toRegisterValue(ctxReg); + Collection parses = assembler.parseLine(text); + for (AssemblyParseResult parse : parses) { + if (!parse.isError()) { + AssemblyResolutionResults sems = assembler.resolveTree(parse, address, ctx); + for (AssemblyResolution ar : sems) { + if (ar.isError()) { + //result.add(new AssemblyError("", ar.toString())); + continue; + } + AssemblyResolvedPatterns rc = (AssemblyResolvedPatterns) ar; + for (byte[] ins : rc.possibleInsVals(ctx)) { + InstructionAssemblyCompletion ai = new InstructionAssemblyCompletion( + program, language, address, text, Arrays.copyOf(ins, ins.length), + ctxVal, computePreference(rc)); + result.add(ai); + if (!exhaustUndefined) { + break; + } + } + } + } + } + } + + @Override + public Collection computeCompletions(String text) { + Set result = new TreeSet<>(); + + collectSuggestionsFromErrors(result, text); + collectSuggestionsFromAccepted(result, text); + + if (result.isEmpty()) { + result.add(new ErrorAssemblyCompletion("", "Invalid instruction and/or prefix")); + } + return result; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyCompletion.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyCompletion.java new file mode 100644 index 0000000000..2919a44c1c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/AssemblyCompletion.java @@ -0,0 +1,97 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler.completion; + +import java.awt.Color; + +/** + * A generic class for all items listed by the autocompleter + */ +public class AssemblyCompletion implements Comparable { + private final String text; + private final String display; + private final Color color; + protected int order; + + public AssemblyCompletion(String text, String display, Color color, int order) { + this.text = text; + this.display = display; + this.color = color; + this.order = order; + } + + /** + * Get the foreground color for the item + * + * @return the color + */ + public Color getColor() { + return color; + } + + /** + * Get the (possibly HTML) text to display for the item + * + * @return the text + */ + public String getDisplay() { + return display; + } + + /** + * Get the text to insert when the item is activated + * + * @return the text + */ + public String getText() { + return text; + } + + /** + * Override this to permit activation by default, i.e., on CTRL-SPACE + * + * @return true to permit defaulting, false to prevent it + */ + public boolean getCanDefault() { + return false; + } + + @Override + public String toString() { + return getDisplay(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AssemblyCompletion)) { + return false; + } + return this.toString().equals(o.toString()); + } + + @Override + public int hashCode() { + return this.toString().hashCode(); + } + + @Override + public int compareTo(AssemblyCompletion that) { + if (this.order != that.order) { + return this.order - that.order; + } + return this.toString().compareTo(that.toString()); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ContextChanges.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ContextChanges.java new file mode 100644 index 0000000000..91267e71ca --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ContextChanges.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.plugin.core.assembler.completion; + +import java.util.Map; +import java.util.TreeMap; + +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.ProgramContext; + +public class ContextChanges implements DisassemblerContextAdapter { + private final RegisterValue contextIn; + final Map contextsOut = new TreeMap<>(); + + public ContextChanges(RegisterValue contextIn) { + this.contextIn = contextIn; + } + + @Override + public RegisterValue getRegisterValue(Register register) { + if (register.getBaseRegister() == contextIn.getRegister()) { + return contextIn.getRegisterValue(register); + } + return null; + } + + @Override + public void setFutureRegisterValue(Address address, RegisterValue value) { + RegisterValue current = contextsOut.get(address); + RegisterValue combined = current == null ? value : current.combineValues(value); + contextsOut.put(address, combined); + } + + public void addFlow(ProgramContext progCtx, Address after) { + contextsOut.put(after, progCtx.getFlowValue(contextIn)); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ErrorAssemblyCompletion.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ErrorAssemblyCompletion.java new file mode 100644 index 0000000000..971aaa830d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/ErrorAssemblyCompletion.java @@ -0,0 +1,38 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler.completion; + +import generic.theme.GThemeDefaults.Colors; + +/** + * Represents the description of an error encountered during parsing or assembling + * + *

    + * NOTE: not used until error descriptions improve + */ +public class ErrorAssemblyCompletion extends AssemblyCompletion { + private String text; + + public ErrorAssemblyCompletion(String text, String desc) { + super(text, desc, Colors.ERROR, 1); + this.text = text; + } + + @Override + public String getText() { + return text; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/InstructionAssemblyCompletion.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/InstructionAssemblyCompletion.java new file mode 100644 index 0000000000..36455206bf --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/InstructionAssemblyCompletion.java @@ -0,0 +1,106 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler.completion; + +import java.math.BigInteger; +import java.util.Map.Entry; + +import ghidra.program.model.address.Address; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.ProgramContext; +import ghidra.program.model.mem.ByteMemBufferImpl; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; + +/** + * Represents an encoding for a complete assembly instruction + * + *

    + * These provide no insertion text, since their activation should be handled by a custom listener. + */ +public class InstructionAssemblyCompletion extends AssemblyCompletion { + private final byte[] data; + final ContextChanges contextChanges; + + public InstructionAssemblyCompletion(Program program, Language language, Address at, String text, + byte[] data, RegisterValue ctxVal, int preference) { + // LATER: Description to display constructor tree information? + super("", NumericUtilities.convertBytesToString(data, " "), switch (preference) { + case 10000 -> AssemblyAutocompleter.FG_PREFERENCE_MOST; + case 5000 -> AssemblyAutocompleter.FG_PREFERENCE_MIDDLE; + default -> AssemblyAutocompleter.FG_PREFERENCE_LEAST; + }, -preference); + this.data = data; + this.contextChanges = new ContextChanges(ctxVal); + + try { + if (program != null) { + // Handle flow context first + contextChanges.addFlow(program.getProgramContext(), at.addWrap(data.length)); + // drop prototype, just want context changes (globalsets) + language.parse(new ByteMemBufferImpl(at, data, language.isBigEndian()), + contextChanges, false); + } + } + catch (InsufficientBytesException | UnknownInstructionException e) { + Msg.error(this, "Cannot disassembly just-assembled instruction?: " + + NumericUtilities.convertBytesToString(data)); + } + adjustOrderByContextChanges(program); + } + + private void adjustOrderByContextChanges(Program program) { + if (program == null) { + return; + } + ProgramContext ctx = program.getProgramContext(); + Register ctxReg = ctx.getBaseContextRegister(); + for (Entry ent : contextChanges.contextsOut.entrySet()) { + RegisterValue defVal = ctx.getDefaultDisassemblyContext(); + RegisterValue newVal = defVal.combineValues(ent.getValue()); + RegisterValue curVal = + defVal.combineValues(ctx.getRegisterValue(ctxReg, ent.getKey())); + BigInteger changed = + newVal.getUnsignedValueIgnoreMask().xor(curVal.getUnsignedValueIgnoreMask()); + order += changed.bitCount(); + } + } + + /** + * Get the assembled instruction bytes + * + * @return the bytes + */ + public byte[] getData() { + return data; + } + + @Override + public int compareTo(AssemblyCompletion ac) { + if (this.order != ac.order) { + return this.order - ac.order; + } + if (!(ac instanceof InstructionAssemblyCompletion)) { + return super.compareTo(ac); + } + InstructionAssemblyCompletion that = (InstructionAssemblyCompletion) ac; + if (this.data.length != that.data.length) { + return this.data.length - that.data.length; + } + return super.compareTo(ac); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/SuggestionAssemblyCompletion.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/SuggestionAssemblyCompletion.java new file mode 100644 index 0000000000..f84645e6c4 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/assembler/completion/SuggestionAssemblyCompletion.java @@ -0,0 +1,30 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.assembler.completion; + +/** + * Represents a textual suggestion to complete or partially complete an assembly instruction + */ +public class SuggestionAssemblyCompletion extends AssemblyCompletion { + public SuggestionAssemblyCompletion(String text, String display) { + super(text, display, null, 1); + } + + @Override + public boolean getCanDefault() { + return true; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java index a0ba1c466f..3346a12466 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clipboard/CodeBrowserClipboardProvider.java @@ -42,6 +42,7 @@ import ghidra.app.cmd.label.RenameLabelCmd; import ghidra.app.plugin.core.codebrowser.CodeViewerActionContext; import ghidra.app.services.ClipboardContentProviderService; import ghidra.app.util.*; +import ghidra.app.util.viewer.field.*; import ghidra.app.util.viewer.listingpanel.ListingModel; import ghidra.framework.model.DomainFile; import ghidra.framework.options.OptionsChangeListener; @@ -79,6 +80,8 @@ public class CodeBrowserClipboardProvider extends ByteCopier new ClipboardType(DataFlavor.stringFlavor, "Memory Block Offset"); public static final ClipboardType CODE_TEXT_TYPE = new ClipboardType(DataFlavor.stringFlavor, "Formatted Code"); + public static final ClipboardType CODE_ASM_TYPE = + new ClipboardType(DataFlavor.stringFlavor, "Assembly Code"); public static final ClipboardType LABELS_COMMENTS_TYPE = new ClipboardType(CodeUnitInfoTransferable.localDataTypeFlavor, "Labels and Comments"); public static final ClipboardType LABELS_TYPE = @@ -101,6 +104,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier List list = new LinkedList<>(); list.add(CODE_TEXT_TYPE); + list.add(CODE_ASM_TYPE); list.add(LABELS_COMMENTS_TYPE); list.add(LABELS_TYPE); list.add(COMMENTS_TYPE); @@ -265,6 +269,9 @@ public class CodeBrowserClipboardProvider extends ByteCopier else if (copyType == CODE_TEXT_TYPE) { return copyCode(monitor); } + else if (copyType == CODE_ASM_TYPE) { + return copyAssembly(monitor); + } else if (copyType == LABELS_COMMENTS_TYPE) { return copyLabelsComments(true, true, monitor); } @@ -521,6 +528,18 @@ public class CodeBrowserClipboardProvider extends ByteCopier return createStringTransferable(g.getBuffer()); } + protected Transferable copyAssembly(TaskMonitor monitor) { + AddressSetView addressSet = getSelectedAddresses(); + StringBuilder builder = new StringBuilder(); + + for (Instruction ins : currentProgram.getListing().getInstructions(addressSet, true)) { + builder.append(ins.toString()); + builder.append("\n"); + } + + return createStringTransferable(builder.toString()); + } + private Transferable copyByteString(Address address) { AddressSet set = new AddressSet(address); return createStringTransferable(copyBytesAsString(set, false, TaskMonitor.DUMMY)); @@ -537,6 +556,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier /** * Gets the Local GhidraURL to the currentLocation within the currentProgram. + * * @return string transferable GhidraURL type. */ private Transferable copyLocalGhidraURL() { @@ -547,6 +567,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier /** * Gets the Shared GhidraURL to the currentLocation within the currentProgram. + * * @return string transferable GhidraURL type. */ private Transferable copySharedGhidraURL() { @@ -929,6 +950,15 @@ public class CodeBrowserClipboardProvider extends ByteCopier return false; } + @Override + public void optionsChanged(ToolOptions options, String optionName, Object oldValue, + Object newValue) throws OptionsVetoException { + if (optionName.equals(ClipboardPlugin.REMOVE_QUOTES_OPTION)) { + includeQuotesForStringData = + !options.getBoolean(ClipboardPlugin.REMOVE_QUOTES_OPTION, false); + } + } + //================================================================================================== // Unsupported Operations //================================================================================================== @@ -1028,13 +1058,4 @@ public class CodeBrowserClipboardProvider extends ByteCopier return flavorList.contains(flavor); } } - - @Override - public void optionsChanged(ToolOptions options, String optionName, Object oldValue, - Object newValue) throws OptionsVetoException { - if (optionName.equals(ClipboardPlugin.REMOVE_QUOTES_OPTION)) { - includeQuotesForStringData = - !options.getBoolean(ClipboardPlugin.REMOVE_QUOTES_OPTION, false); - } - } } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTestHelper.java b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTestHelper.java index 5392fdf3a9..2c131b138f 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTestHelper.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/assembler/AssemblerPluginTestHelper.java @@ -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. @@ -24,8 +24,8 @@ import javax.swing.JTextField; import docking.test.AbstractDockingTest; import generic.test.AbstractGTest; import generic.test.AbstractGuiTest; -import ghidra.app.plugin.core.assembler.AssemblyDualTextField.AssemblyCompletion; -import ghidra.app.plugin.core.assembler.AssemblyDualTextField.AssemblyInstruction; +import ghidra.app.plugin.core.assembler.completion.AssemblyCompletion; +import ghidra.app.plugin.core.assembler.completion.InstructionAssemblyCompletion; import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; import ghidra.app.util.viewer.listingpanel.ListingPanel; import ghidra.program.model.address.Address; @@ -68,7 +68,7 @@ public class AssemblerPluginTestHelper { } public void snuffWarning(Language language) { - PatchInstructionAction.SHOWN_WARNING.put(language, true); + AssemblerPlugin.SHOWN_WARNING.put(language, true); } public void assertDualFields() { @@ -123,8 +123,8 @@ public class AssemblerPluginTestHelper { List completions = inputAndGetCompletions(newText); assertFalse("There are no assembly completion options", completions.isEmpty()); AssemblyCompletion first = completions.get(0); - assertTrue(first instanceof AssemblyInstruction); - AssemblyInstruction ai = (AssemblyInstruction) first; + assertTrue(first instanceof InstructionAssemblyCompletion); + InstructionAssemblyCompletion ai = (InstructionAssemblyCompletion) first; AbstractGuiTest.runSwing(() -> patchInstructionAction.accept(ai)); AbstractGhidraHeadedIntegrationTest.waitForProgram(program); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionCellRenderer.java index aa41ac3952..74424382f9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionCellRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionCellRenderer.java @@ -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. @@ -26,24 +26,24 @@ import javax.swing.JList; import javax.swing.ListCellRenderer; /** - * This is a default list cell renderer for the {@link TextFieldAutocompleter} suitable for + * This is a default list cell renderer for the {@link TextComponentAutocompleter} suitable for * extension if a user wishes to customize it. * * Mostly, this just composes Swing's {@link DefaultListCellRenderer}, except it allows each * suggested item to specify its own text, font, icon, foreground color, and background color. Of * course, the display text may also use HTML tags for fine formatting. * @param the type of items suggested by the autocompleter. - * @see TextFieldAutocompleter + * @see TextComponentAutocompleter */ public class AutocompletionCellRenderer implements ListCellRenderer { - private final TextFieldAutocompleter owner; + private final TextComponentAutocompleter owner; protected ListCellRenderer defaultRenderer = new DefaultListCellRenderer(); /** * Create a renderer owned by the given autocompleter. * @param owner the autocompleter that uses (or will use) this renderer. */ - public AutocompletionCellRenderer(TextFieldAutocompleter owner) { + public AutocompletionCellRenderer(TextComponentAutocompleter owner) { this.owner = owner; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionEvent.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionEvent.java index 6ad2716a0d..fc1dce545b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionEvent.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionEvent.java @@ -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. @@ -15,34 +15,36 @@ */ package docking.widgets.autocomplete; -import javax.swing.JTextField; +import javax.swing.text.JTextComponent; /** * An event related to autocompletion, usually a completion being activated by the user. * * @param the type of suggestions given by the autocompleter. - * @see TextFieldAutocompleter + * @see TextComponentAutocompleter */ public class AutocompletionEvent { private T sel; - private JTextField field; + private JTextComponent component; private boolean consumed; private boolean cancelled; /** - * Create a new event on the given selection and text field. + * Create a new event on the given selection and text component. + * * @param sel the currently-selected (or activated) item. - * @param field the field having focus at the time of the event. + * @param component the component having focus at the time of the event. */ - public AutocompletionEvent(T sel, JTextField field) { + public AutocompletionEvent(T sel, JTextComponent component) { this.sel = sel; - this.field = field; + this.component = component; } /** * Get the item that was selected at the time of the event. * * For activation, this is the activated suggestion. + * * @return the selected suggestion. */ public T getSelection() { @@ -50,14 +52,15 @@ public class AutocompletionEvent { } /** - * Get the field having focus at the time of the event. + * Get the component having focus at the time of the event. * - * If the autocompleter is attached to multiple fields, this can be used to identify which - * field produced the event. - * @return the focused field + * If the autocompleter is attached to multiple components, this can be used to identify which + * component produced the event. + * + * @return the focused component */ - public JTextField getField() { - return field; + public JTextComponent getComponent() { + return component; } /** @@ -71,6 +74,7 @@ public class AutocompletionEvent { /** * Check if this event has been consumed by an earlier listener. + * * @return true if the event has been consumed, i.e., should not be further processed. */ public boolean isConsumed() { @@ -88,6 +92,7 @@ public class AutocompletionEvent { /** * Check if the actual completion action will be performed. + * * @return true if the completion action has been cancelled. */ public boolean isCancelled() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionListener.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionListener.java index 864343825a..50e11f5610 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionListener.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/AutocompletionListener.java @@ -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. @@ -19,7 +19,7 @@ package docking.widgets.autocomplete; * A listener for autocompletion events. * * @param the type of suggestions presented by the autocompleter. - * @see TextFieldAutocompleter + * @see TextComponentAutocompleter */ public interface AutocompletionListener { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextComponentAutocompleter.java similarity index 85% rename from Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java rename to Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextComponentAutocompleter.java index d0fa9b42b6..63e02ab770 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextFieldAutocompleter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/autocomplete/TextComponentAutocompleter.java @@ -25,8 +25,7 @@ import java.util.stream.IntStream; import javax.swing.*; import javax.swing.border.BevelBorder; import javax.swing.event.*; -import javax.swing.text.BadLocationException; -import javax.swing.text.Caret; +import javax.swing.text.*; import docking.DockingUtils; import docking.DockingUtils.TreeTraversalResult; @@ -36,27 +35,28 @@ import generic.util.WindowUtilities; import ghidra.util.task.SwingUpdateManager; /** - * An autocompleter that may be attached to one or more {@link JTextField}. + * An autocompleter that may be attached to one or more {@link JTextComponent}s. * *

    * Each autocompleter instance has one associated window (displaying the list of suggestions) and * one associated model (generating the list of suggestions). Thus, the list can only be active on - * one of the attached text fields at a time. This is usually the desired behavior, and it allows - * for one autocompleter to be reused on many fields. Behavior is undefined when multiple - * autocompleters are attached to the same text field. More likely, you should implement a composite - * model if you wish to present completions from multiple models on a single text field. + * one of the attached text components at a time. This is usually the desired behavior, and it + * allows for one autocompleter to be reused on many components. Behavior is undefined when multiple + * autocompleters are attached to the same text component. More likely, you should implement a + * composite model if you wish to present completions from multiple models on a single text + * component. * *

    * By default, the autocompleter is activated when the user presses CTRL-SPACE, at which point, the * model is queried for possible suggestions. The completer gives the model all the text preceding - * the current field's caret. This behavior can be changed by overriding the - * {@link #getPrefix(JTextField)} method. This may be useful, e.g., to obtain a prefix for the - * current word, rather than the full field contents, preceding the caret. The list is displayed - * such that its top-left corner is placed directly under the current field's caret. As the user + * the current component's caret. This behavior can be changed by overriding the + * {@link #getPrefix(JTextComponent)} method. This may be useful, e.g., to obtain a prefix for the + * current word, rather than the full component contents, preceding the caret. The list is displayed + * such that its top-left corner is placed directly under the current component's caret. As the user * continues typing, the suggestions are re-computed, and the list tracks with the caret. This * positioning behavior can be modified by overriding the {@link #getCompletionWindowPosition()} - * method. As a convenience, the {@link #getCaretPositionOnScreen(JTextField)} method is available - * to compute the default position. + * method. As a convenience, the {@link #getCaretPositionOnScreen(JTextComponent)} method is + * available to compute the default position. * *

    * Whether or not the list is currently displayed, when the user presses CTRL-SPACE, if only one @@ -64,8 +64,8 @@ import ghidra.util.task.SwingUpdateManager; * until either no suggestions are given, or more than one suggestion is given (or until the * autocompleter detects an infinite loop). This behavior can by modified on an item-by-item basis * by overriding the {@link #getCompletionCanDefault(Object) getCompletionCanDefault(T)} method. - * This same behavior can be activated by calling the {@link #startCompletion(JTextField)} method, - * which may be useful, e.g., to bind a different key sequence to start autocompletion. + * This same behavior can be activated by calling the {@link #startCompletion(JTextComponent)} + * method, which may be useful, e.g., to bind a different key sequence to start autocompletion. * *

    * The appearance of each item in the suggestion list can be modified by overriding the various @@ -94,7 +94,7 @@ import ghidra.util.task.SwingUpdateManager; * * @param the type of suggestions presented by this autocompleter. */ -public class TextFieldAutocompleter { +public class TextComponentAutocompleter { private static final int DEFAULT_UPDATE_DELAY = 10; private static final int DEFAULT_MAX_UPDATE_DELAY = 2000; // TODO: Maybe compute the default dimensions based on content? @@ -105,8 +105,8 @@ public class TextFieldAutocompleter { // Variables to keep track of state private final AutocompletionModel model; - private final Set attachees = new HashSet<>(); - private JTextField focus; + private final Set attachees = new HashSet<>(); + private JTextComponent focus; private List> autocompletionListeners = new ArrayList<>(); // Swing fodder @@ -276,7 +276,7 @@ public class TextFieldAutocompleter { * * @param model the model giving the suggestions. */ - public TextFieldAutocompleter(AutocompletionModel model) { + public TextComponentAutocompleter(AutocompletionModel model) { this.model = model; } @@ -410,9 +410,9 @@ public class TextFieldAutocompleter { * @param field an attached field, usually the one with focus. * @return the prefix to use as the query. */ - protected String getPrefix(JTextField field) { + protected String getPrefix(JTextComponent component) { try { - return field.getText(0, field.getCaretPosition()); + return component.getText(0, component.getCaretPosition()); } catch (BadLocationException e) { throw new AssertionError("INTERNAL: Should not be here", e); @@ -423,10 +423,11 @@ public class TextFieldAutocompleter { * Get the preferred location (on screen) of the completion list window. * *

    - * Typically, this is a location near the focused field. Ideally, it is positioned such that the - * displayed suggestions coincide with the applicable text in the focused field. For example, if - * the suggestions display some portion of the prefix, the window could be positioned such that - * the portion in the suggestion appears directly below the same portion in the field. + * Typically, this is a location near the focused component. Ideally, it is positioned such that + * the displayed suggestions coincide with the applicable text in the focused component. For + * example, if the suggestions display some portion of the prefix, the window could be + * positioned such that the portion in the suggestion appears directly below the same portion in + * the component. * * @return the point giving the top-left corner of the completion window */ @@ -438,7 +439,7 @@ public class TextFieldAutocompleter { * Get the preferred dimensions of the completion list window. * *

    - * Typically, this is the width of the focused field. + * Typically, this is the width of the focused component. * * @return the dimension giving the preferred height and width. A value can be -1 to indicate no * preference. @@ -451,27 +452,28 @@ public class TextFieldAutocompleter { } /** - * A convenience function that returns the bottom on-screen position of the given field's caret. + * A convenience function that returns the bottom on-screen position of the given component's + * caret. * - * @param field the field, typically the one having focus - * @return the on-screen position of the caret's bottom; null if the given field is null + * @param component the component, typically the one having focus + * @return the on-screen position of the caret's bottom; null if the given component is null */ - protected Point getCaretPositionOnScreen(JTextField field) { + protected Point getCaretPositionOnScreen(JTextComponent component) { if (focus == null) { return null; } - FontMetrics metrics = field.getFontMetrics(field.getFont()); - Caret c = field.getCaret(); + FontMetrics metrics = component.getFontMetrics(component.getFont()); + Caret c = component.getCaret(); Point p = c.getMagicCaretPosition(); // returns a shared reference if (p == null) { - p = new Point(0, field.getBaseline(1, 1)); + p = new Point(0, component.getBaseline(1, 1)); } else { p = new Point(p); } p.y += metrics.getHeight(); - SwingUtilities.convertPointToScreen(p, field); + SwingUtilities.convertPointToScreen(p, component); return p; } @@ -492,48 +494,48 @@ public class TextFieldAutocompleter { } /** - * Attach the autocompleter to the given text field. + * Attach the autocompleter to the given text component. * *

    * If this method is never called, then the autocompleter can never appear. * - * @param field the field that will gain this autocompletion feature - * @return true, if this field is not already attached + * @param component the component that will gain this autocompletion feature + * @return true, if this component is not already attached */ - public boolean attachTo(JTextField field) { - if (!attachees.add(field)) { + public boolean attachTo(JTextComponent component) { + if (!attachees.add(component)) { return false; } boolean keep = false; try { - field.addFocusListener(listener); - field.addCaretListener(listener); - field.addKeyListener(listener); - field.getDocument().addDocumentListener(listener); + component.addFocusListener(listener); + component.addCaretListener(listener); + component.addKeyListener(listener); + component.getDocument().addDocumentListener(listener); keep = true; } finally { if (!keep) { - attachees.remove(field); + attachees.remove(component); } } return keep; } /** - * Deprive the given field of this autocompleter. + * Deprive the given component of this autocompleter. * - * @param field the field that will lose this autocompletion feature - * @return true, if this field was actually attached + * @param component the component that will lose this autocompletion feature + * @return true, if this component was actually attached */ - public boolean detachFrom(JTextField field) { - if (!attachees.remove(field)) { + public boolean detachFrom(JTextComponent component) { + if (!attachees.remove(component)) { return false; } - field.removeFocusListener(listener); - field.removeCaretListener(listener); - field.removeKeyListener(listener); - field.getDocument().removeDocumentListener(listener); + component.removeFocusListener(listener); + component.removeCaretListener(listener); + component.removeKeyListener(listener); + component.getDocument().removeDocumentListener(listener); return true; } @@ -729,11 +731,11 @@ public class TextFieldAutocompleter { * Decide whether the given suggestion can be automatically activated. * *

    - * When autocompletion is started (via {@link #startCompletion(JTextField)}) or when the user - * presses CTRL-SPACE, if there is only a single suggestion, it is taken automatically, and the - * process repeats until there is not a sole suggestion. Before the suggestion is taken, though, - * it calls this method. If it returns false, the single suggestion is displayed in a 1-long - * list instead. This is useful to prevent consequential actions from being automatically + * When autocompletion is started (via {@link #startCompletion(JTextComponent)}) or when the + * user presses CTRL-SPACE, if there is only a single suggestion, it is taken automatically, and + * the process repeats until there is not a sole suggestion. Before the suggestion is taken, + * though, it calls this method. If it returns false, the single suggestion is displayed in a + * 1-long list instead. This is useful to prevent consequential actions from being automatically * activated by the autocompleter. * * @param sel the potentially auto-activated suggestion. @@ -744,27 +746,27 @@ public class TextFieldAutocompleter { } /** - * Starts the autocompleter on the given text field. + * Starts the autocompleter on the given text component. * *

    * First, this repeatedly attempts auto-activation. When there are many suggestions, or when * auto-activation is prevented (see {@link #getCompletionCanDefault(Object) * getCompletionCanDefault(T)}), a list is displayed (usually below the caret) containing the - * suggestions given the fields current contents. The list remains open until either the user - * cancels it (usually via ESC) or the user activates a suggestion. + * suggestions given the components current contents. The list remains open until either the + * user cancels it (usually via ESC) or the user activates a suggestion. * *

    - * NOTE: The text field must already be attached. + * NOTE: The text component must already be attached. * - * @param field the field on which to start autocompletion. + * @param component the component on which to start autocompletion. */ - public void startCompletion(JTextField field) { - if (!attachees.contains(field)) { - throw new IllegalArgumentException("Given field is not attached"); + public void startCompletion(JTextComponent component) { + if (!attachees.contains(component)) { + throw new IllegalArgumentException("Given component is not attached"); } Set visited = new HashSet<>(); while (true) { - String before = getPrefix(field); + String before = getPrefix(component); if (!visited.add(before)) { return; } @@ -964,7 +966,7 @@ public class TextFieldAutocompleter { } else if (e.getKeyCode() == KeyEvent.VK_SPACE && (e.getModifiersEx() & DockingUtils.CONTROL_KEY_MODIFIER_MASK) != 0) { - startCompletion((JTextField) e.getComponent()); + startCompletion((JTextComponent) e.getComponent()); e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { @@ -982,19 +984,19 @@ public class TextFieldAutocompleter { @Override public void focusGained(FocusEvent e) { - focus = (JTextField) e.getComponent(); + focus = (JTextComponent) e.getComponent(); updateDisplayContents(); } - /*test*/ public void fakeFocusGained(JTextField field) { - focus = field; + /*test*/ public void fakeFocusGained(JTextComponent component) { + focus = component; } @Override public void focusLost(FocusEvent e) { Component opp = e.getOppositeComponent(); if (attachees.contains(opp)) { - focus = (JTextField) opp; + focus = (JTextComponent) opp; } else if (opp == list) { // Do nothing @@ -1068,15 +1070,15 @@ public class TextFieldAutocompleter { * The autocompleter offers the tails from a list of strings that start with the text before the * caret. */ - public static class TextFieldAutocompleterDemo { + public static class TextComponentAutocompleterDemo { public static void main(String[] args) { JDialog dialog = new JDialog((Window) null, "Autocompleter Demo"); JTextField field = new JTextField(); dialog.add(field); - TextFieldAutocompleter auto = - new TextFieldAutocompleter<>(new AutocompletionModel() { + TextComponentAutocompleter auto = + new TextComponentAutocompleter<>(new AutocompletionModel() { Set strings = new HashSet<>(Arrays.asList(new String[] { "Test", "Testing", "Another", "Yet another", "Yet still more" })); { @@ -1114,7 +1116,8 @@ public class TextFieldAutocompleter { */ public static class DualTextAutocompleterDemo { public static void main(String[] args) { - JDialog dialog = new JDialog((Window) null, "MultiTextField with Autocompleter Demo"); + JDialog dialog = + new JDialog((Window) null, "Linked TextFields with Autocompleter Demo"); Box hbox = Box.createHorizontalBox(); dialog.add(hbox); @@ -1143,10 +1146,10 @@ public class TextFieldAutocompleter { return matching; } }; - TextFieldAutocompleter auto = new TextFieldAutocompleter<>(model) { + TextComponentAutocompleter auto = new TextComponentAutocompleter<>(model) { @Override - protected String getPrefix(JTextField field) { - return dual.getTextBeforeCursor(field); + protected String getPrefix(JTextComponent component) { + return dual.getTextBeforeCursor((JTextField) component); } }; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java index 445ec8d6f5..43c6e2ab2e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/TextFieldLinker.java @@ -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. @@ -400,6 +400,7 @@ public class TextFieldLinker { * Set the composite text * * @param text the new text + * @return the adjustment to the caret position */ public int setText(String text) { int adj = 0; @@ -497,6 +498,9 @@ public class TextFieldLinker { /** * @see #linkField(JTextField, Pattern, String) + * @param field the field to link + * @param exp the regular expression to search for following the field + * @param sep the separator that replaces {@code pat} when matched */ public void linkField(JTextField field, String exp, String sep) { Pattern pat = Pattern.compile(exp); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java index 0e351cd914..7725ae0321 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/AssemblyBuffer.java @@ -17,10 +17,17 @@ package ghidra.app.plugin.assembler; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; import ghidra.app.plugin.assembler.sleigh.sem.AssemblyPatternBlock; +import ghidra.app.plugin.processors.sleigh.SleighParserContext; +import ghidra.app.util.PseudoInstruction; import ghidra.program.model.address.Address; -import ghidra.program.model.listing.Program; +import ghidra.program.model.address.AddressOverflowException; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.*; +import ghidra.program.util.ProgramContextImpl; /** * A convenience for accumulating bytes output by an {@link Assembler} @@ -45,9 +52,86 @@ import ghidra.program.model.listing.Program; * */ public class AssemblyBuffer { + static class FlowBits { + final byte[] noFlow; + + public FlowBits(byte[] noFlow) { + this.noFlow = noFlow; + } + + static void setBits(byte[] into, byte[] from) { + for (int i = 0; i < into.length; i++) { + into[i] |= from[i]; + } + } + + static void clrBits(byte[] into, byte[] from) { + for (int i = 0; i < into.length; i++) { + into[i] &= ~from[i]; + } + } + + static boolean applySubMask(byte[] noFlow, Register reg) { + boolean result = false; + byte[] subMask = reg.getBaseMask(); + if (!reg.followsFlow()) { + setBits(noFlow, subMask); + return true; + } + // Don't set anything, just descend + if (!reg.hasChildren()) { + return result; + } + for (Register child : reg.getChildRegisters()) { + result |= applySubMask(noFlow, child); + } + return result; + } + + static FlowBits fromBase(Register ctxreg) { + byte[] noFlow = ctxreg.getBaseMask().clone(); + Arrays.fill(noFlow, (byte) 0); + if (applySubMask(noFlow, ctxreg)) { + return new FlowBits(noFlow); + } + return new FlowBits(null); + } + + public RegisterValue getFlowValue(RegisterValue value) { + if (noFlow == null) { + return value; + } + return value.clearBitValues(noFlow); + } + } + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); private final Assembler asm; + private final Language language; + private final Register ctxreg; private final Address entry; + private final ProgramContext progCtx; + private final FlowBits flowBits; + + /** + * Create a buffer with the given assembler starting at the given entry + * + * @param asm the assembler + * @param entry the starting address where the resulting code will be located + * @param initialContext optional override of context at entry, null to take default + */ + public AssemblyBuffer(Assembler asm, Address entry, RegisterValue initialContext) { + this.asm = asm; + this.language = asm.getLanguage(); + this.entry = entry; + this.ctxreg = language.getContextBaseRegister(); + this.progCtx = ctxreg == null || ctxreg == Register.NO_CONTEXT + ? null + : new ProgramContextImpl(language); + this.flowBits = progCtx == null ? null : FlowBits.fromBase(ctxreg); + + copyEntryContext(initialContext); + } /** * Create a buffer with the given assembler starting at the given entry @@ -56,8 +140,35 @@ public class AssemblyBuffer { * @param entry the starting address where the resulting code will be located */ public AssemblyBuffer(Assembler asm, Address entry) { - this.asm = asm; - this.entry = entry; + this(asm, entry, null); + } + + private RegisterValue getEntryContext(RegisterValue override) { + if (progCtx == null) { + return null; + } + Program program = asm.getProgram(); + if (program == null) { + return override; + } + RegisterValue ctx = program.getProgramContext().getDisassemblyContext(entry); + if (ctx == null) { + return override; + } + return ctx.combineValues(override); // handles null + } + + private void copyEntryContext(RegisterValue override) { + RegisterValue ctx = getEntryContext(override); + if (ctx == null) { + return; + } + try { + progCtx.setRegisterValue(entry, entry, ctx); + } + catch (ContextChangeException e) { + throw new AssertionError(e); + } } /** @@ -69,6 +180,62 @@ public class AssemblyBuffer { return entry.add(baos.size()); } + private byte[] applyContext(Address start, byte[] insBytes, AssemblyPatternBlock ctx) { + if (progCtx == null) { + return insBytes; + } + MemBuffer buf = new ByteMemBufferImpl(start, insBytes, language.isBigEndian()); + ProcessorContext procCtx = new ProgramProcessorContext(progCtx, start); + try { + InstructionPrototype prototype = language.parse(buf, procCtx, false); + PseudoInstruction ins = new PseudoInstruction(start, prototype, buf, procCtx); + if (ins.hasFallthrough()) { + RegisterValue rvCtx = ctx.toRegisterValue(ctxreg); + progCtx.setRegisterValue(start, start, rvCtx); + Address fall = ins.getFallThrough(); + progCtx.setRegisterValue(fall, fall, flowBits.getFlowValue(rvCtx)); + } + if (!(ins.getParserContext() instanceof SleighParserContext parserCtx)) { + return insBytes; + } + parserCtx.applyCommits(procCtx); + } + catch (InsufficientBytesException | UnknownInstructionException | MemoryAccessException + | AddressOverflowException | ContextChangeException e) { + throw new AssertionError(e); + } + return insBytes; + } + + public AssemblyPatternBlock getNextCtx() { + return getCtx(getNext(), null); + } + + private AssemblyPatternBlock getCtx(Address at, AssemblyPatternBlock override) { + if (ctxreg == null || ctxreg == Register.NO_CONTEXT) { + return null; + } + AssemblyPatternBlock result = asm.getContextAt(at); + + RegisterValue fromProc = progCtx.getRegisterValue(ctxreg, at); + if (fromProc != null) { + result = result.assign(AssemblyPatternBlock.fromRegisterValue(fromProc)); + } + if (override != null) { + result.assign(override); + } + return result.fillMask(); + } + + private byte[] doAssembleLine(Address at, String line, AssemblyPatternBlock ctx) + throws AssemblySyntaxException, AssemblySemanticException { + ctx = getCtx(at, ctx); + return applyContext(at, ctx == null + ? asm.assembleLine(at, line) + : asm.assembleLine(at, line, ctx), + ctx); + } + /** * Assemble a line and append it to the buffer * @@ -81,7 +248,7 @@ public class AssemblyBuffer { */ public byte[] assemble(String line, AssemblyPatternBlock ctx) throws AssemblySyntaxException, AssemblySemanticException, IOException { - return emit(asm.assembleLine(getNext(), line, ctx)); + return emit(doAssembleLine(getNext(), line, ctx)); } /** @@ -95,7 +262,7 @@ public class AssemblyBuffer { */ public byte[] assemble(String line) throws AssemblySyntaxException, AssemblySemanticException, IOException { - return emit(asm.assembleLine(getNext(), line)); + return assemble(line, null); } /** @@ -134,7 +301,7 @@ public class AssemblyBuffer { public byte[] assemble(Address at, String line, AssemblyPatternBlock ctx) throws AssemblySyntaxException, AssemblySemanticException, IOException { byte[] full = baos.toByteArray(); - byte[] bytes = asm.assembleLine(at, line, ctx); + byte[] bytes = doAssembleLine(at, line, ctx); System.arraycopy(bytes, 0, full, (int) at.subtract(entry), bytes.length); baos.reset(); baos.write(full); @@ -154,12 +321,7 @@ public class AssemblyBuffer { */ public byte[] assemble(Address at, String line) throws AssemblySyntaxException, AssemblySemanticException, IOException { - byte[] full = baos.toByteArray(); - byte[] bytes = asm.assembleLine(at, line); - System.arraycopy(bytes, 0, full, (int) at.subtract(entry), bytes.length); - baos.reset(); - baos.write(full); - return bytes; + return assemble(at, line, null); } /** diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/GenericAssembler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/GenericAssembler.java index 412ce2151b..58b9a1fd7c 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/GenericAssembler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/GenericAssembler.java @@ -61,8 +61,8 @@ public interface GenericAssembler { * @throws AddressOverflowException the resulting block is beyond the valid address range */ public InstructionIterator assemble(Address at, String... listing) - throws AssemblySyntaxException, - AssemblySemanticException, MemoryAccessException, AddressOverflowException; + throws AssemblySyntaxException, AssemblySemanticException, MemoryAccessException, + AddressOverflowException; /** * Assemble a line instruction at the given address. diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/sem/AssemblyPatternBlock.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/sem/AssemblyPatternBlock.java index 48e9a64918..517e5679fe 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/sem/AssemblyPatternBlock.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/sem/AssemblyPatternBlock.java @@ -30,6 +30,7 @@ import ghidra.app.plugin.processors.sleigh.expression.ContextField; import ghidra.app.plugin.processors.sleigh.expression.TokenField; import ghidra.app.plugin.processors.sleigh.pattern.DisjointPattern; import ghidra.app.plugin.processors.sleigh.pattern.PatternBlock; +import ghidra.program.model.lang.Register; import ghidra.program.model.lang.RegisterValue; import ghidra.util.NumericUtilities; @@ -634,8 +635,8 @@ public class AssemblyPatternBlock implements Comparable { /** * Write mask bits from context commit to mask array of block * - * @implNote This is used when scraping for valid input contexts to determine which context variables - * are passed to the globalset directive. + * @implNote This is used when scraping for valid input contexts to determine which context + * variables are passed to the globalset directive. * * @param cc the context commit * @return the result @@ -836,7 +837,7 @@ public class AssemblyPatternBlock implements Comparable { * Mask the given {@code unmasked} value with the mask contained in this pattern block. * *

    - * The returned {@link AssemblyPatternBlock} has an identical mask as {@code this} but with a + * The returned {@link AssemblyPatternBlock} has an identical mask as {@code this} but with a * value taken from the given {@code unmasked}. * * @param unmasked the value to be masked into the result @@ -979,6 +980,16 @@ public class AssemblyPatternBlock implements Comparable { return res; } + /** + * Convert this pattern block into a register value + * + * @param reg the register, typically the contextreg + * @return the register value + */ + public RegisterValue toRegisterValue(Register reg) { + return new RegisterValue(reg, toBigInteger(reg.getNumBytes())); + } + /** * Counts the total number of known bits in the pattern * diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/symbol/AssemblyNumericTerminal.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/symbol/AssemblyNumericTerminal.java index c92dc0f5d9..dbd5d26aaa 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/symbol/AssemblyNumericTerminal.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/symbol/AssemblyNumericTerminal.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import ghidra.app.plugin.assembler.sleigh.grammars.AssemblyGrammar; import ghidra.app.plugin.assembler.sleigh.tree.AssemblyParseNumericToken; +import ghidra.app.plugin.assembler.sleigh.tree.AssemblyParseSymbolToken; import ghidra.program.model.address.AddressSpace; /** @@ -140,7 +141,7 @@ public class AssemblyNumericTerminal extends AssemblyTerminal { String lab = buffer.substring(s, b); return symbols.choose(lab, space) .stream() - .map(val -> new AssemblyParseNumericToken(grammar, this, lab, val)) + .map(val -> new AssemblyParseSymbolToken(grammar, this, lab, val)) .collect(Collectors.toList()); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/tree/AssemblyParseSymbolToken.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/tree/AssemblyParseSymbolToken.java new file mode 100644 index 0000000000..a9b79ac154 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/assembler/sleigh/tree/AssemblyParseSymbolToken.java @@ -0,0 +1,26 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.assembler.sleigh.tree; + +import ghidra.app.plugin.assembler.sleigh.grammars.AssemblyGrammar; +import ghidra.app.plugin.assembler.sleigh.symbol.AssemblyTerminal; + +public class AssemblyParseSymbolToken extends AssemblyParseNumericToken { + public AssemblyParseSymbolToken(AssemblyGrammar grammar, AssemblyTerminal term, String str, + long val) { + super(grammar, term, str, val); + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/processors/sleigh/ContextCache.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/processors/sleigh/ContextCache.java index 639898eccb..8741927884 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/processors/sleigh/ContextCache.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/plugin/processors/sleigh/ContextCache.java @@ -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. @@ -111,8 +111,7 @@ public class ContextCache { // ctx.setValue(contextBaseRegister, addr, new BigInteger(bytes)); // } public void setContext(ProcessorContext ctx, Address addr, int num, int mask, int value) { - if (ctx instanceof DisassemblerContext) { - DisassemblerContext context = (DisassemblerContext) ctx; + if (ctx instanceof DisassemblerContext context) { int byteSize = context_size * 4; byte[] bytes = new byte[2 * byteSize]; putInt(bytes, byteSize + num * 4, value); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/ReDisassembler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/ReDisassembler.java index 75ea601050..f488344154 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/ReDisassembler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/ReDisassembler.java @@ -136,17 +136,16 @@ public class ReDisassembler { * instruction and context matches what's already there, or it encounters an unconditional * branch. * - * @return true if the queue is non-empty after completing this block, false if we're done. * @throws CancelledException */ - protected boolean nextBlock() throws CancelledException { + protected void nextBlock() throws CancelledException { monitor.checkCancelled(); - instructionSet.addBlock(new ReDisBlock(this, queue.pop()).disassembleBlock()); - return !queue.isEmpty(); + instructionSet.addBlock(new ReDisBlock(this, queue.pollFirst()).disassembleBlock()); } protected InstructionSet disassemble() throws CancelledException { - while (nextBlock()) { + while (!queue.isEmpty()) { + nextBlock(); } return instructionSet; } @@ -361,10 +360,8 @@ public class ReDisassembler { } } - public AddressSetView disasemble(Address seed, TaskMonitor monitor) throws CancelledException { - ReDisState state = new ReDisState(monitor); - state.addSeed(seed); - InstructionSet set = state.disassemble(); + protected AddressSetView placeInstructions(ReDisState state, InstructionSet set, + TaskMonitor monitor) throws CancelledException { for (AddressRange range : set.getAddressSet()) { listing.clearCodeUnits(range.getMinAddress(), range.getMaxAddress(), true, monitor); } @@ -377,4 +374,21 @@ public class ReDisassembler { } return set.getAddressSet(); } + + public AddressSetView disassemble(AddressSetView seeds, TaskMonitor monitor) + throws CancelledException { + ReDisState state = new ReDisState(monitor); + for (Address seed : seeds.getAddresses(true)) { + state.addSeed(seed); + } + InstructionSet set = state.disassemble(); + return placeInstructions(state, set, monitor); + } + + public AddressSetView disassemble(Address seed, TaskMonitor monitor) throws CancelledException { + ReDisState state = new ReDisState(monitor); + state.addSeed(seed); + InstructionSet set = state.disassemble(); + return placeInstructions(state, set, monitor); + } } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java index 8689027ac3..4e49013527 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ClipboardPluginScreenShots.java @@ -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. @@ -41,7 +41,7 @@ import ghidra.util.exception.AssertException; public class ClipboardPluginScreenShots extends GhidraScreenShotGenerator { @Test - public void testCaptureCopySpecial() { + public void testCaptureCopySpecial() throws InterruptedException { CodeBrowserPlugin cb = getPlugin(tool, CodeBrowserPlugin.class); ProgramSelection sel = cb.getCurrentSelection(); @@ -56,6 +56,8 @@ public class ClipboardPluginScreenShots extends GhidraScreenShotGenerator { Dimension size = window.getSize(); setWindowSize(window, size.width, 330); + + Thread.sleep(2); // Time to manually set focus between resume and capture captureDialog(); } diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/disassemble/ReDisassemblerTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/disassemble/ReDisassemblerTest.java index 9edfff9867..a69e44f95e 100644 --- a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/disassemble/ReDisassemblerTest.java +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/program/disassemble/ReDisassemblerTest.java @@ -70,7 +70,7 @@ public class ReDisassemblerTest extends AbstractGhidraHeadlessIntegrationTest { "imm r0, #0x23"); ReDisassembler rd = new ReDisassembler(program); - rd.disasemble(start, monitor); + rd.disassemble(start, monitor); } } }