Files
rea/tests/conformance/ghidra/ReaArmContextProbe.java
T
CyberSprout ee0ec027db feat(firmware): attribute hardware MMIO peripheral memory regions in native disassembly (#5348)
* feat(firmware): attribute hardware MMIO peripheral memory regions in native disassembly

- introduce hardwareMemoryMap domain model supporting standard ARM Cortex-M, RISC-V, MIPS, x86, and ARM64 physical memory region classifications (Code, SRAM, Peripheral MMIO, System Control Space, External Device)
- distinguish RAM/ROM and address-space segments from MMIO (isolate ARM External RAM / Device and MIPS kseg0/kseg1 uncached mirrors and boot ROM as non-MMIO)
- add hardwarePeripheralAttribution to map analyzed instruction references to hardware peripheral blocks and summarize peripheral register interaction profiles with numerical offset ordering
- expand ReaGhidraBridge.java decoder_context to capture ARM TMode execution register alongside MIPS ISA_MODE for native instruction inspection
- add focused unit and boundary tests across hardwareMemoryMap.test.ts, hardwarePeripheralAttribution.test.ts, GhidraFunctionValues.test.ts, and ghidraCompactReferencesMcp.test.ts verifying region bounds, MMIO isolation, numerical sorting, 32-bit/64-bit overflow rejection, ARM TMode MCP preservation, and bridge contract bindings
- make fake Ghidra installation host platform-aware on Windows in GhidraLanguageOverride.test.ts

* test(ghidra): inspect ARM and Thumb context in the Java bridge

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-23 05:37:58 +08:00

102 lines
5.5 KiB
Java

//@category REA Verification
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import ghidra.app.script.GhidraScript;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramContext;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
/** Real ARM/Thumb decoding; only the unavailable context value is injected. */
public class ReaArmContextProbe extends GhidraScript {
@Override
public void run() throws Exception {
String[] args = getScriptArgs();
require(args.length == 1, "Expected the exact production bridge directory");
var source = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java");
require(source != null && new File(source.getAbsolutePath()).getCanonicalFile().equals(
new File(args[0], "ReaGhidraBridge.java").getCanonicalFile()), "Production bridge source");
GhidraScript bridge = GhidraScriptUtil.getProvider(source).getScriptInstance(source, errorWriter);
bridge.set(getState(), getControls());
require(currentProgram.getLanguageID().getIdAsString().equals("ARM:LE:32:v7"), "ARM fixture language");
var mode = currentProgram.getRegister("TMode");
require(mode != null, "Real TMode register");
Address arm = currentProgram.getMemory().getMinAddress();
Address thumb = arm.add(4);
var context = currentProgram.getProgramContext();
context.setValue(mode, arm, arm.add(3), BigInteger.ZERO);
context.setValue(mode, thumb, thumb.add(1), BigInteger.ONE);
require(disassemble(arm), "ARM instruction decoded");
require(disassemble(thumb), "Thumb instruction decoded");
Field space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace");
space.setAccessible(true);
space.set(null, arm.getAddressSpace());
Method inspect = bridge.getClass().getDeclaredMethod("inspectNativeInstruction", JsonObject.class, boolean.class);
inspect.setAccessible(true);
check(inspect, bridge, arm, "0", 4, "1eff2fe1");
check(inspect, bridge, thumb, "1", 2, "7047");
// getValue includes language defaults. Inject only its unavailable result,
// preserving the real instruction, register, language and all other APIs.
Program original = currentProgram;
ProgramContext unavailable = (ProgramContext) Proxy.newProxyInstance(
ProgramContext.class.getClassLoader(), new Class<?>[] { ProgramContext.class },
(proxy, method, values) -> method.getName().equals("getValue") && mode.equals(values[0])
? null : invoke(method, context, values));
Program program = (Program) Proxy.newProxyInstance(Program.class.getClassLoader(),
new Class<?>[] { Program.class }, (proxy, method, values) ->
method.getName().equals("getProgramContext") ? unavailable : invoke(method, original, values));
Field programField = field(bridge.getClass(), "currentProgram");
programField.setAccessible(true);
try {
programField.set(bridge, program);
check(inspect, bridge, arm, null, 4, "1eff2fe1");
} finally {
programField.set(bridge, original);
}
println("REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)");
}
private static void check(Method inspect, Object bridge, Address address, String mode,
int length, String bytes) throws Exception {
JsonObject input = new JsonObject();
input.add("document", JsonNull.INSTANCE);
input.addProperty("address", "0x" + Long.toUnsignedString(address.getOffset(), 16));
JsonObject result = (JsonObject) inspect.invoke(bridge, input, false);
require(result.get("status").getAsString().equals("decoded"), "Decoded bridge response");
require(result.get("length").getAsInt() == length, "ARM/Thumb instruction width");
require(result.get("bytes").getAsString().equals(bytes), "Original instruction bytes");
var contexts = result.getAsJsonArray("decoder_context");
require(contexts != null && contexts.size() == 1, "One retained mode context");
var value = contexts.get(0).getAsJsonObject();
require(value.get("register").getAsString().equals("TMode"), "TMode register name");
require(value.get("source").getAsString().equals("ghidra-program-context"), "Context provenance");
require(mode == null ? value.get("value").isJsonNull()
: mode.equals(value.get("value").getAsString()), "Exact mode value " + mode);
}
private static Object invoke(Method method, Object owner, Object[] values) throws Throwable {
try { return method.invoke(owner, values); }
catch (InvocationTargetException failure) { throw failure.getCause(); }
}
private static Field field(Class<?> owner, String name) throws NoSuchFieldException {
for (Class<?> type = owner; type != null; type = type.getSuperclass()) {
try { return type.getDeclaredField(name); }
catch (NoSuchFieldException absent) { /* Check the inherited API owner. */ }
}
throw new NoSuchFieldException(name);
}
private static void require(boolean condition, String label) {
if (!condition) throw new IllegalStateException(label);
}
}