diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index 82d75c5cc23..1733dc60f4c 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -605,6 +605,11 @@ private JsonObject sessionInfo(SessionDescriptor descriptor) throws Exception { String riscvCallingConvention = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO) .getString("REA RISC-V calling convention", null); if (riscvCallingConvention != null) result.add("riscv_calling_convention", JsonParser.parseString(riscvCallingConvention)); + // Written by ReaGhidraPrepareElfLoad before auto-analysis; REA requires it + // exactly when the committed profile declares a RISC-V load preparation. + String loadPreparation = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO) + .getString("REA ELF load preparation", null); + if (loadPreparation != null) result.add("elf_load_preparation", JsonParser.parseString(loadPreparation)); return result; } diff --git a/bridge/ghidra/ReaGhidraPrepareElfLoad.java b/bridge/ghidra/ReaGhidraPrepareElfLoad.java new file mode 100644 index 00000000000..62d4bf9753c --- /dev/null +++ b/bridge/ghidra/ReaGhidraPrepareElfLoad.java @@ -0,0 +1,358 @@ +import com.google.gson.JsonObject; +import ghidra.app.script.ImproperUseException; +import ghidra.app.util.headless.HeadlessScript; +import ghidra.program.database.ProgramDB; +import ghidra.program.database.mem.FileBytes; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressFactory; +import ghidra.program.model.address.AddressIterator; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.mem.MemoryBlockSourceInfo; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceManager; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.model.symbol.SymbolTable; +import ghidra.program.model.symbol.SymbolType; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Give an executable section that a NOBITS reservation shares addresses with the default address + * space before auto-analysis (docs/providers/ghidra-riscv.md). Ghidra 12.1.4's ELF loader resolves the + * overlap in favour of the later section, so ESP-IDF's .flash_rodata_dummy keeps the default space + * and .flash.text lands in an overlay, which DWARF import, call-frame entries and default-space + * references never reach. REA's TypeScript admission commits both sections, and the script refuses + * any other memory map, and then turns off headless auto-analysis, since REA discards the session. + * The file bytes and their offsets do not change. + */ +public final class ReaGhidraPrepareElfLoad extends HeadlessScript { + static final String POLICY = "nobits-reservation-overlay-v1"; + static final String REPORT_KEY = "REA ELF load preparation"; + /** The handshake bounds a refusal; this leaves room for the context around a failure. */ + private static final int FAILURE_DETAIL_MAX = 512; + + private record Label(String name, long offset, Namespace namespace, SourceType source) {} + + @Override + public void run() throws Exception { + String[] arguments = getScriptArgs(); + if (arguments.length != 6 || !arguments[0].equals(POLICY)) { + throw new IllegalArgumentException( + "ELF load preparation requires policy " + POLICY + " and the committed code and reservation sections" + ); + } + long codeAddress = parseHex(arguments[1]); + long codeSize = parseHex(arguments[2]); + long codeOffset = parseHex(arguments[3]); + long reservationAddress = parseHex(arguments[4]); + long reservationSize = parseHex(arguments[5]); + if (codeSize == 0 || reservationAddress != codeAddress || + Long.compareUnsigned(reservationSize, codeSize) < 0) { + throw new IllegalArgumentException( + "ELF load preparation requires a reservation that starts with the code and covers it" + ); + } + Memory memory = currentProgram.getMemory(); + AddressSpace space = currentProgram.getAddressFactory().getDefaultAddressSpace(); + List files = memory.getAllFileBytes(); + if (files.size() != 1) { + refuse("Ghidra holds " + files.size() + " imported file images instead of one"); + return; + } + FileBytes file = files.get(0); + MemoryBlock code = null; + MemoryBlock reservation = null; + int codeCandidates = 0; + for (MemoryBlock block : memory.getBlocks()) { + AddressSpace blockSpace = block.getStart().getAddressSpace(); + if (block.getStart().getOffset() != codeAddress) continue; + if (block.isInitialized() && block.isExecute() && block.getSize() == codeSize && + blockSpace.getPhysicalSpace() == space && mapsFile(block, file, codeOffset)) { + code = block; + codeCandidates++; + } + if (!block.isInitialized() && !block.isExecute() && blockSpace == space && + block.getSize() == reservationSize) { + reservation = block; + } + } + String committed = "executable section " + hex(codeAddress) + " (" + hex(codeSize) + + " bytes at file offset " + hex(codeOffset) + ")"; + if (codeCandidates != 1) { + refuse("Ghidra has " + codeCandidates + " initialized executable blocks for the committed " + committed); + return; + } + if (!code.isOverlay() && reservation == null) { + JsonObject result = new JsonObject(); + result.add("code", echoCode(code, code.getStart().getAddressSpace().getName())); + report("canonical", result); + return; + } + if (!code.isOverlay() || reservation == null) { + refuse("Ghidra placed the committed " + committed + (code.isOverlay() + ? " in an overlay, but no uninitialized default-space reservation " + hex(reservationAddress) + + " (" + hex(reservationSize) + " bytes) displaced it" + : " in the default space beside a reservation block")); + return; + } + // The code's whole overlay space moves with it and then becomes the reservation's. + for (MemoryBlock block : memory.getBlocks()) { + if (block != code && block.getStart().getAddressSpace().equals(code.getStart().getAddressSpace())) { + refuse("Ghidra placed block " + block.getName() + " in the overlay space of the committed " + committed); + return; + } + } + JsonObject moved; + try { + moved = swap(code, reservation, file, codeOffset, committed); + } + catch (Exception failure) { + // A failed swap can leave the program half moved: REA must not analyze it. If Ghidra + // rolls back the failed script's changes, the refusal goes too, and the missing report + // refuses the session instead (not tested). + refuse("Ghidra failed while moving the committed " + committed + ": " + detail(failure)); + throw failure; + } + if (moved != null) report("moved", moved); + } + + /** + * Ghidra 12.1.4 constraints: Memory.moveBlock keeps a block in its address space, and the + * native decompiler stops registering programs once any overlay space is removed. The code + * therefore moves by re-creating its block and moving program content, and its overlay space + * becomes the reservation's. Returns null after a refusal. + */ + private JsonObject swap(MemoryBlock code, MemoryBlock reservation, FileBytes file, long fileOffset, + String committed) throws Exception { + Memory memory = currentProgram.getMemory(); + SymbolTable symbols = currentProgram.getSymbolTable(); + AddressSpace space = currentProgram.getAddressFactory().getDefaultAddressSpace(); + long size = code.getSize(); + long reservationSize = reservation.getSize(); + String codeName = code.getName(); + String reservationName = reservation.getName(); + AddressSpace overlaySpace = code.getStart().getAddressSpace(); + String overlayName = overlaySpace.getName(); + // Everything in the code's overlay space belongs to the code, including labels past its + // end, such as ESP-IDF's section-relative _etext. + AddressSet old = new AddressSet(overlaySpace.getMinAddress(), overlaySpace.getMaxAddress()); + List