From b382017ccba121e141322c9ab0f378f8ed5d63eb Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:04:42 -0500 Subject: [PATCH] GP-4014: Context and disasembly re-flow after patching instruction. --- .../RepairDisassemblyScript.java | 25 ++ .../cmd/disassemble/ReDisassembleCommand.java | 44 +++ .../assembler/PatchInstructionAction.java | 5 + .../program/disassemble/Disassembler.java | 2 +- .../program/disassemble/ReDisassembler.java | 366 ++++++++++++++++++ .../lang}/DisassemblerContextAdapter.java | 3 +- 6 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/cmd/disassemble/ReDisassembleCommand.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/ReDisassembler.java rename Ghidra/Framework/{Emulation/src/main/java/ghidra/pcode/emu => SoftwareModeling/src/main/java/ghidra/program/model/lang}/DisassemblerContextAdapter.java (97%) diff --git a/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java b/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java new file mode 100644 index 0000000000..60dc43af41 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/RepairDisassemblyScript.java @@ -0,0 +1,25 @@ +/* ### + * 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. + */ +import ghidra.app.script.GhidraScript; +import ghidra.program.disassemble.ReDisassembler; + +public class RepairDisassemblyScript extends GhidraScript { + @Override + protected void run() throws Exception { + ReDisassembler dis = new ReDisassembler(currentProgram); + dis.disasemble(currentAddress, monitor); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/disassemble/ReDisassembleCommand.java b/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/disassemble/ReDisassembleCommand.java new file mode 100644 index 0000000000..1e7f347efb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/disassemble/ReDisassembleCommand.java @@ -0,0 +1,44 @@ +/* ### + * 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.cmd.disassemble; + +import ghidra.framework.cmd.BackgroundCommand; +import ghidra.framework.model.DomainObject; +import ghidra.program.disassemble.ReDisassembler; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class ReDisassembleCommand extends BackgroundCommand { + private final Address seed; + + public ReDisassembleCommand(Address seed) { + this.seed = seed; + } + + @Override + public boolean applyTo(DomainObject obj, TaskMonitor monitor) { + ReDisassembler dis = new ReDisassembler((Program) obj); + try { + dis.disasemble(seed, monitor); + return true; + } + catch (CancelledException e) { + return false; + } + } +} 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 0a8f57607d..5f01183ef4 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 @@ -34,6 +34,7 @@ import docking.widgets.autocomplete.*; import docking.widgets.fieldpanel.*; import docking.widgets.fieldpanel.support.FieldLocation; 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.*; @@ -301,7 +302,11 @@ public class PatchInstructionAction extends AbstractPatchAction { } protected void applyPatch(byte[] data) throws MemoryAccessException { + // 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()); + tool.executeBackgroundCommand(cmd, getProgram()); } /** diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/Disassembler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/Disassembler.java index 8311545970..1992482da4 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/Disassembler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/Disassembler.java @@ -1355,7 +1355,7 @@ public class Disassembler implements DisassemblerConflictHandler { * @return true if the call also falls through to this instruction */ private boolean isNoReturnCall(Instruction instr, Address target) { - // if allready overriden, return + // if already overridden, return // is this function a call fixup if (program == null) { return false; // can't tell without program 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 new file mode 100644 index 0000000000..c1945049ba --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/ReDisassembler.java @@ -0,0 +1,366 @@ +/* ### + * 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.program.disassemble; + +import java.util.*; + +import ghidra.app.util.PseudoInstruction; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.util.CodeUnitInsertionException; +import ghidra.program.util.ProgramContextImpl; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A class that re-disassembles where necessary + * + *

+ * Given a seed address, this will (re-)disassemble the instruction at that address. If it indicates + * any context changes, whether via {@code globalset} or fall-through, the affected addresses are + * considered for re-disassembly as well. If no instruction exists at the address, or an off-cut + * instruction exists at the address, the address is dropped, but the outgoing context is recorded. + * If one does exist, but its context is already the same, the address is dropped. Otherwise, it is + * queued up and the process repeats. + */ +public class ReDisassembler { + + protected final Language language; + protected final AddressFactory addrFactory; + protected final Register ctxRegister; + protected final ParallelInstructionLanguageHelper parallelHelper; + + private final Program program; + private final Listing listing; + private final ProgramContext programContext; + + public ReDisassembler(Program program) { + this.program = program; + this.listing = program.getListing(); + this.programContext = program.getProgramContext(); + this.language = program.getLanguage(); + this.addrFactory = program.getAddressFactory(); + this.ctxRegister = language.getContextBaseRegister(); + this.parallelHelper = language.getParallelInstructionHelper(); + } + + enum FlowType { + SEED, FALLTHROUGH, BRANCH, GLOBALSET; + } + + record Flow(Address from, Address to, FlowType type) { + static Flow seed(Address seed) { + return new Flow(Address.NO_ADDRESS, seed, FlowType.SEED); + } + + static Flow fallThrough(Instruction instruction) throws AddressOverflowException { + return new Flow(instruction.getAddress(), + instruction.getAddress().add(instruction.getLength()), FlowType.FALLTHROUGH); + } + + static Flow branch(Instruction instruction, Address to) { + return new Flow(instruction.getAddress(), to, FlowType.BRANCH); + } + + Flow globalSet(Address address) { + return new Flow(to, address, FlowType.GLOBALSET); + } + } + + protected class ReDisState { + protected final TaskMonitor monitor; + protected final MemBuffer progMemBuffer = + new DumbMemBufferImpl(program.getMemory(), program.getMemory().getMinAddress()); + protected final ProgramContext tempContext = new ProgramContextImpl(language); + protected final AddressSet visited = new AddressSet(); + protected final Deque queue = new LinkedList<>(); + protected final InstructionSet instructionSet = new InstructionSet(addrFactory); + // ProgramContext will not remember sets to default context. + protected final Set

ctxAddrs = new TreeSet<>(); + + public ReDisState(TaskMonitor monitor) { + this.monitor = monitor; + this.monitor.setMessage("Re-disassembling"); + } + + protected ReDisState addSeed(Address seed) { + RegisterValue seedCtx = programContext.getRegisterValue(ctxRegister, seed); + try { + tempContext.setRegisterValue(seed, seed, seedCtx); + } + catch (ContextChangeException e) { + throw new AssertionError(e); + } + return addFlow(Flow.seed(seed)); + } + + protected ReDisState addFlow(Flow flow) { + queue.add(flow); + return this; + } + + protected MemBuffer createBuffer(Address at) { + return new WrappedMemBuffer(progMemBuffer, 20, + (int) at.subtract(progMemBuffer.getAddress())); + } + + /** + * Not necessarily a full block, but certainly no more than a block. + * + *

+ * It's also not necessarily a basic block, since this doesn't care about jumps + * into the block. It simply starts at the next seed and proceeds until either the existing + * 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 { + monitor.checkCancelled(); + instructionSet.addBlock(new ReDisBlock(this, queue.pop()).disassembleBlock()); + return !queue.isEmpty(); + } + + protected InstructionSet disassemble() throws CancelledException { + while (nextBlock()) { + } + return instructionSet; + } + + public void writeContext() { + for (Address addr : ctxAddrs) { + RegisterValue curCtxVal = programContext.getRegisterValue(ctxRegister, addr); + System.err.println("Writing context at " + addr); + System.err.println(" Cur: " + curCtxVal); + RegisterValue newCtxVal = tempContext.getRegisterValue(ctxRegister, addr); + System.err.println(" New: " + newCtxVal); + + if (curCtxVal.equals(newCtxVal)) { + continue; + } + try { + programContext.setRegisterValue(addr, addr, newCtxVal); + } + catch (ContextChangeException e) { + Msg.error(this, "Cannot write context at " + addr + ": " + e); + } + } + } + } + + protected class ReDisBlock { + protected final ReDisState state; + protected final Flow entry; + protected final InstructionBlock block; + protected final DisassemblerContextImpl disassemblerContext; + protected PseudoInstruction lastInstruction; + + public ReDisBlock(ReDisState state, Flow entry) { + this.state = state; + this.entry = entry; + this.block = new InstructionBlock(entry.to); + this.disassemblerContext = new DisassemblerContextImpl(state.tempContext); + this.disassemblerContext.flowStart(entry.to); + } + + protected void recordContext(Address to) { + try { + state.tempContext.setRegisterValue(to, to, + disassemblerContext.getRegisterValue(ctxRegister)); + state.ctxAddrs.add(to); + } + catch (ContextChangeException e) { + throw new AssertionError(e); + } + } + + protected PseudoInstruction createInstruction(Address address, + InstructionPrototype prototype, MemBuffer memBuffer, ProcessorContext ctx) + throws AddressOverflowException { + PseudoInstruction instruction = new PseudoInstruction(program, address, prototype, + memBuffer, ctx); + instruction.setInstructionBlock(block); + System.err.println( + " " + instruction + " with " + instruction.getRegisterValue(ctxRegister)); + return lastInstruction = instruction; + } + + protected boolean shouldDisassemble(Flow flow) { + Instruction exists = listing.getInstructionContaining(flow.to); + if (exists == null) { + /** + * NOTE: New instructions are not placed until we're all done, so there is no need + * to worry about differences in instruction length as we progress through a block. + */ + return false; + } + if (flow.type == FlowType.FALLTHROUGH) { + return true; + } + if (exists.getAddress().equals(flow.to)) { + return true; + } + return false; + } + + protected Instruction nextInstruction(Flow flow, boolean isInDelaySlot) + throws CancelledException { + state.monitor.checkCancelled(); + if (state.visited.contains(flow.to)) { + // Already disassembled here + // TODO: Record instruction conflicts? + return null; + } + recordContext(flow.to); + if (!shouldDisassemble(flow)) { + return null; + } + MemBuffer buffer = state.createBuffer(flow.to); + RegisterValue newCtxVal = disassemblerContext.getRegisterValue(ctxRegister); + RegisterValue curCtxVal = programContext.getRegisterValue(ctxRegister, flow.to); + if (newCtxVal.equals(curCtxVal) && flow.type != FlowType.SEED) { + // No need to re-disassemble if context has not changed. + return null; + } + System.err.println("Re-disassembling " + flow + " with " + newCtxVal); + ReDisassemblerContext ctx = new ReDisassemblerContext(state, flow); + try { + InstructionPrototype prototype = language.parse(buffer, ctx, false); + return createInstruction(flow.to, prototype, buffer, ctx); + } + catch (UnknownInstructionException e) { + block.setParseConflict(flow.from, disassemblerContext.getRegisterValue( + disassemblerContext.getBaseContextRegister()), flow.to, e.getMessage()); + return null; + } + catch (InsufficientBytesException e) { + block.setInstructionMemoryError(flow.to, flow.from, e.getMessage()); + return null; + } + catch (AddressOverflowException e) { + block.setInstructionMemoryError(flow.to, flow.from, + "Instruction does not fit within address space constraint"); + return null; + } + } + + /** + * Parse the next instructions, including delay-slotted ones + * + * @param address the starting address + * @return the if the first instruction has fall-through, the flow out from the last + * instruction parsed. Without delay slots, the first instruction is the last + * instruction. + * @throws CancelledException + * @throws AddressOverflowException if an instruction would run past the end of the address + * space + */ + protected Flow nextInstructionsWithDelays(Flow flow) throws CancelledException { + Instruction instruction = nextInstruction(flow, false); + if (instruction == null) { + return null; + } + boolean hasFallthrough = instruction.hasFallthrough(); + try { + flow = Flow.fallThrough(instruction); + block.addInstruction(instruction); + processInstruction(instruction); + disassemblerContext.flowToAddress(flow.to); + int remainingBytes = instruction.getPrototype().getDelaySlotByteCount(); + while (remainingBytes > 0) { + instruction = nextInstruction(flow, true); + if (instruction == null) { + return null; + } + flow = Flow.fallThrough(instruction); + block.addInstruction(instruction); + processInstruction(instruction); + remainingBytes -= instruction.getLength(); + } + return hasFallthrough ? flow : null; + } + catch (AddressOverflowException e) { + block.setInstructionMemoryError(flow.to, flow.from, + "Failed to properly process delay slot at end of address space"); + return null; + } + } + + protected void processInstruction(Instruction instruction) { + state.visited.add(instruction.getMinAddress(), instruction.getMaxAddress()); + for (Address to : instruction.getFlows()) { + recordContext(to); + state.addFlow(Flow.branch(instruction, to)); + } + } + + protected InstructionBlock disassembleBlock() throws CancelledException { + Flow flow = entry; + while (flow != null) { + flow = nextInstructionsWithDelays(flow); + } + return block; + } + } + + protected class ReDisassemblerContext implements DisassemblerContextAdapter { + protected final ReDisState state; + protected final Flow flow; + + public ReDisassemblerContext(ReDisState state, Flow flow) { + this.state = state; + this.flow = flow; + } + + @Override + public void setFutureRegisterValue(Address address, RegisterValue value) { + state.addFlow(flow.globalSet(address)); + try { + state.tempContext.setRegisterValue(address, address, value); + state.ctxAddrs.add(address); + } + catch (ContextChangeException e) { + throw new AssertionError(e); + } + } + + @Override + public RegisterValue getRegisterValue(Register register) { + return state.tempContext.getRegisterValue(register, flow.to); + } + } + + public AddressSetView disasemble(Address seed, TaskMonitor monitor) throws CancelledException { + ReDisState state = new ReDisState(monitor); + state.addSeed(seed); + InstructionSet set = state.disassemble(); + for (AddressRange range : set.getAddressSet()) { + listing.clearCodeUnits(range.getMinAddress(), range.getMaxAddress(), true, monitor); + } + state.writeContext(); + try { + listing.addInstructions(set, false); + } + catch (CodeUnitInsertionException e) { + Msg.error(this, "Could not overwrite with re-disassembly", e); + } + return set.getAddressSet(); + } +} diff --git a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DisassemblerContextAdapter.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/DisassemblerContextAdapter.java similarity index 97% rename from Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DisassemblerContextAdapter.java rename to Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/DisassemblerContextAdapter.java index 1ff2c7233c..25638916be 100644 --- a/Ghidra/Framework/Emulation/src/main/java/ghidra/pcode/emu/DisassemblerContextAdapter.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/DisassemblerContextAdapter.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pcode.emu; +package ghidra.program.model.lang; import java.math.BigInteger; import java.util.List; import ghidra.program.model.address.Address; -import ghidra.program.model.lang.*; import ghidra.program.model.listing.ContextChangeException; public interface DisassemblerContextAdapter extends DisassemblerContext {