mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
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>
This commit is contained in:
@@ -25,16 +25,16 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
- name: Validate the SDK pin against the provider's verified release
|
||||
run: node scripts/verify-ghidra-bridge-sources.mjs --check-sdk-pin >> "$GITHUB_ENV"
|
||||
# Only the jars and application.properties are extracted: the check
|
||||
# compiles against them and never launches Ghidra or imports a target.
|
||||
- name: Acquire checksum-verified Ghidra jars in isolated runner storage
|
||||
# The bridge probe imports only source-owned ARM/Thumb bytes and never
|
||||
# executes target code. All SDK/project state stays in runner storage.
|
||||
- name: Acquire checksum-verified Ghidra SDK in isolated runner storage
|
||||
shell: bash
|
||||
run: |
|
||||
ghidra_root="${RUNNER_TEMP}/rea-ghidra"
|
||||
mkdir -p "${ghidra_root}"
|
||||
curl -fsSL --retry 3 --write-out 'ghidra_download_bytes=%{size_download} ghidra_download_seconds=%{time_total}\n' -o "${ghidra_root}/ghidra.zip" "${GHIDRA_RELEASE_URL}"
|
||||
echo "${GHIDRA_RELEASE_SHA256} ${ghidra_root}/ghidra.zip" | sha256sum -c -
|
||||
unzip -q "${ghidra_root}/ghidra.zip" '*/Ghidra/*.jar' '*/Ghidra/application.properties' -d "${ghidra_root}"
|
||||
unzip -q "${ghidra_root}/ghidra.zip" -d "${ghidra_root}"
|
||||
rm "${ghidra_root}/ghidra.zip"
|
||||
properties="$(find "${ghidra_root}" -path '*/Ghidra/application.properties' -print -quit)"
|
||||
test -n "${properties}"
|
||||
@@ -46,3 +46,19 @@ jobs:
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
test -n "${!java_home_variable}"
|
||||
JAVA_HOME="${!java_home_variable}" npm run verify:ghidra:bridge-sources -- --verified-sdk
|
||||
- name: Inspect real ARM and Thumb instruction context through the bridge
|
||||
shell: bash
|
||||
run: |
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
export JAVA_HOME="${!java_home_variable}"
|
||||
probe_root="${RUNNER_TEMP}/rea-arm-context"
|
||||
mkdir -p "${probe_root}/project"
|
||||
printf '\036\377\057\341\160\107\000\000' > "${probe_root}/arm-thumb.bin"
|
||||
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/project" ArmContext \
|
||||
-import "${probe_root}/arm-thumb.bin" -loader BinaryLoader -processor ARM:LE:32:v7 \
|
||||
-noanalysis -readOnly -deleteProject \
|
||||
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
-postScript ReaArmContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
> "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; }
|
||||
cat "${probe_root}/output.log"
|
||||
rg -F 'REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)' "${probe_root}/output.log"
|
||||
|
||||
@@ -1268,9 +1268,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint())));
|
||||
result.addProperty("architecture", currentProgram.getLanguageID().getIdAsString());
|
||||
result.addProperty("mode", currentProgram.getLanguage().getLanguageDescription().getVariant());
|
||||
if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")) {
|
||||
if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:") ||
|
||||
currentProgram.getLanguageID().getIdAsString().startsWith("ARM:")) {
|
||||
JsonArray decoderContext = new JsonArray();
|
||||
var modeRegister = currentProgram.getRegister("ISA_MODE");
|
||||
String regName = currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:") ? "ISA_MODE" : "TMode";
|
||||
var modeRegister = currentProgram.getRegister(regName);
|
||||
if (modeRegister != null) {
|
||||
var modeValue = currentProgram.getProgramContext().getValue(modeRegister, address, false);
|
||||
JsonObject context = new JsonObject();
|
||||
|
||||
@@ -140,6 +140,13 @@ try {
|
||||
address: entry,
|
||||
});
|
||||
assert.equal(instruction.normalized_result.address, entry);
|
||||
assert.ok(Array.isArray(instruction.normalized_result.decoder_context));
|
||||
const tmodeContext = instruction.normalized_result.decoder_context.find(
|
||||
(ctx) => ctx.register === "TMode",
|
||||
);
|
||||
assert.ok(tmodeContext !== undefined);
|
||||
assert.equal(tmodeContext.source, "ghidra-program-context");
|
||||
assert.ok([null, "0", "1"].includes(tmodeContext.value));
|
||||
}
|
||||
assert.equal(profiles[0], profiles[1]);
|
||||
assert.notEqual(profiles[1], profiles[2]);
|
||||
|
||||
@@ -209,3 +209,6 @@ export const firmwareResultSchemas = {
|
||||
published_bytes: bytes,
|
||||
}),
|
||||
} as const;
|
||||
|
||||
export * from "./hardwareMemoryMap.js";
|
||||
export * from "./hardwarePeripheralAttribution.js";
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
classifyHardwareMemoryAddress,
|
||||
isHardwareMmio,
|
||||
parseAddressValue,
|
||||
} from "./hardwareMemoryMap.js";
|
||||
|
||||
describe("ARM Cortex-M hardware memory map classification", () => {
|
||||
it.each([
|
||||
{
|
||||
address: "0x0",
|
||||
kind: "code",
|
||||
name: "Code / Flash",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x08000000",
|
||||
kind: "code",
|
||||
name: "Code / Flash",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x20000000",
|
||||
kind: "sram",
|
||||
name: "Internal SRAM",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x40000000",
|
||||
kind: "peripheral_mmio",
|
||||
name: "Peripheral MMIO",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x40011000",
|
||||
kind: "peripheral_mmio",
|
||||
name: "Peripheral MMIO",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x60000000",
|
||||
kind: "external_device",
|
||||
name: "External RAM",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0xa0000000",
|
||||
kind: "external_device",
|
||||
name: "External Device Space",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0xe000e010",
|
||||
kind: "system_control",
|
||||
name: "System Control Space (SCS)",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0xe000e100",
|
||||
kind: "system_control",
|
||||
name: "System Control Space (SCS)",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0xe0100000",
|
||||
kind: "reserved",
|
||||
name: "Reserved Architecture Space",
|
||||
isMmio: false,
|
||||
},
|
||||
])(
|
||||
"classifies ARM address $address as $kind ($name)",
|
||||
({ address, kind, name, isMmio }) => {
|
||||
const result = classifyHardwareMemoryAddress(address, "arm");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.regionKind).toBe(kind);
|
||||
expect(result.value.regionName).toBe(name);
|
||||
expect(result.value.isMmio).toBe(isMmio);
|
||||
expect(isHardwareMmio(address, "arm")).toBe(isMmio);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("RISC-V and MIPS hardware memory map classification", () => {
|
||||
it.each([
|
||||
{
|
||||
address: "0x00001000",
|
||||
kind: "code",
|
||||
name: "Debug & Boot ROM",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x02000000",
|
||||
kind: "system_control",
|
||||
name: "Core Local Interruptor (CLINT)",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x0c000000",
|
||||
kind: "system_control",
|
||||
name: "Platform-Level Interrupt Controller (PLIC)",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x10012000",
|
||||
kind: "peripheral_mmio",
|
||||
name: "Peripheral MMIO",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x20000000",
|
||||
kind: "code",
|
||||
name: "Flash Memory",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x80000000",
|
||||
kind: "sram",
|
||||
name: "Main System RAM",
|
||||
isMmio: false,
|
||||
},
|
||||
])(
|
||||
"classifies RISC-V address $address",
|
||||
({ address, kind, name, isMmio }) => {
|
||||
const result = classifyHardwareMemoryAddress(address, "riscv");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.regionKind).toBe(kind);
|
||||
expect(result.value.regionName).toBe(name);
|
||||
expect(result.value.isMmio).toBe(isMmio);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
address: "0x00400000",
|
||||
kind: "code",
|
||||
name: "kuseg (User TLB Space)",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x80000000",
|
||||
kind: "sram",
|
||||
name: "kseg0 (Kernel Unmapped Cached)",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0xa0000000",
|
||||
kind: "sram",
|
||||
name: "kseg1 (Kernel Unmapped Uncached)",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0xbfc00000",
|
||||
kind: "code",
|
||||
name: "kseg1 Boot ROM / Reset Vector",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0xc0000000",
|
||||
kind: "system_control",
|
||||
name: "kseg2 / kseg3 (Kernel TLB Space)",
|
||||
isMmio: false,
|
||||
},
|
||||
])("classifies MIPS address $address", ({ address, kind, name, isMmio }) => {
|
||||
const result = classifyHardwareMemoryAddress(address, "mips");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.regionKind).toBe(kind);
|
||||
expect(result.value.regionName).toBe(name);
|
||||
expect(result.value.isMmio).toBe(isMmio);
|
||||
});
|
||||
});
|
||||
|
||||
describe("x86 and ARM64 hardware memory map classification", () => {
|
||||
it.each([
|
||||
{
|
||||
address: "0x00000000",
|
||||
kind: "sram",
|
||||
name: "Real Mode Base RAM",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x000a0000",
|
||||
kind: "peripheral_mmio",
|
||||
name: "Video MMIO (VGA)",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x000e0000",
|
||||
kind: "code",
|
||||
name: "Option ROM / BIOS Code",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0xfec00000",
|
||||
kind: "system_control",
|
||||
name: "APIC System Control MMIO",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0xfffffff0",
|
||||
kind: "code",
|
||||
name: "BIOS Firmware Flash",
|
||||
isMmio: false,
|
||||
},
|
||||
])("classifies x86 address $address", ({ address, kind, name, isMmio }) => {
|
||||
const result = classifyHardwareMemoryAddress(address, "x86");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.regionKind).toBe(kind);
|
||||
expect(result.value.regionName).toBe(name);
|
||||
expect(result.value.isMmio).toBe(isMmio);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
address: "0x0000000000001000",
|
||||
kind: "code",
|
||||
name: "Flash / Firmware Code",
|
||||
isMmio: false,
|
||||
},
|
||||
{
|
||||
address: "0x0000000008000000",
|
||||
kind: "system_control",
|
||||
name: "Interrupt Controller (GIC) MMIO",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x0000000009000000",
|
||||
kind: "peripheral_mmio",
|
||||
name: "Peripheral MMIO Space",
|
||||
isMmio: true,
|
||||
},
|
||||
{
|
||||
address: "0x0000000040000000",
|
||||
kind: "sram",
|
||||
name: "System RAM / DRAM",
|
||||
isMmio: false,
|
||||
},
|
||||
])("classifies ARM64 address $address", ({ address, kind, name, isMmio }) => {
|
||||
const result = classifyHardwareMemoryAddress(address, "arm64");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.regionKind).toBe(kind);
|
||||
expect(result.value.regionName).toBe(name);
|
||||
expect(result.value.isMmio).toBe(isMmio);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hardware memory map address parsing and overflow boundaries", () => {
|
||||
it("rejects 32-bit address overflow on 32-bit architectures", () => {
|
||||
const overflow = classifyHardwareMemoryAddress("0x100000000", "arm");
|
||||
expect(overflow.ok).toBe(false);
|
||||
if (overflow.ok) return;
|
||||
expect(overflow.error).toContain("exceeds arm maximum address width");
|
||||
|
||||
const mipsOverflow = classifyHardwareMemoryAddress("0x100000000", "mips");
|
||||
expect(mipsOverflow.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("admits 64-bit addresses on arm64 and rejects 64-bit overflow", () => {
|
||||
const valid64 = classifyHardwareMemoryAddress(
|
||||
"0x0000000040000000",
|
||||
"arm64",
|
||||
);
|
||||
expect(valid64.ok).toBe(true);
|
||||
|
||||
const overflow64 = parseAddressValue("0x10000000000000000");
|
||||
expect(overflow64.ok).toBe(false);
|
||||
if (overflow64.ok) return;
|
||||
expect(overflow64.error).toContain("hexadecimal string");
|
||||
});
|
||||
|
||||
it("rejects malformed address strings and negative BigInt values", () => {
|
||||
expect(parseAddressValue("not-hex").ok).toBe(false);
|
||||
expect(parseAddressValue("0x").ok).toBe(false);
|
||||
expect(parseAddressValue("").ok).toBe(false);
|
||||
expect(parseAddressValue(-1n).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("handles BigInt address inputs directly", () => {
|
||||
const result = classifyHardwareMemoryAddress(0x40001000n, "arm");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.canonicalAddress).toBe("0x40001000");
|
||||
expect(result.value.isMmio).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,415 @@
|
||||
import { z } from "zod";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
|
||||
/** Supported target hardware CPU architectures for memory map classification. */
|
||||
export const HARDWARE_ARCHITECTURES = [
|
||||
"arm",
|
||||
"arm64",
|
||||
"riscv",
|
||||
"mips",
|
||||
"x86",
|
||||
] as const;
|
||||
export type HardwareArchitecture = (typeof HARDWARE_ARCHITECTURES)[number];
|
||||
export const hardwareArchitectureSchema = z.enum(HARDWARE_ARCHITECTURES);
|
||||
|
||||
/** Standard hardware memory region functional classifications. */
|
||||
export const HARDWARE_MEMORY_REGION_KINDS = [
|
||||
"code",
|
||||
"sram",
|
||||
"peripheral_mmio",
|
||||
"external_device",
|
||||
"system_control",
|
||||
"reserved",
|
||||
] as const;
|
||||
export type HardwareMemoryRegionKind =
|
||||
(typeof HARDWARE_MEMORY_REGION_KINDS)[number];
|
||||
export const hardwareMemoryRegionKindSchema = z.enum(
|
||||
HARDWARE_MEMORY_REGION_KINDS,
|
||||
);
|
||||
|
||||
/** Internal descriptor for an architectural memory map segment. */
|
||||
export interface HardwareMemoryRegionDefinition {
|
||||
readonly name: string;
|
||||
readonly kind: HardwareMemoryRegionKind;
|
||||
readonly start: bigint;
|
||||
readonly end: bigint;
|
||||
readonly isMmio: boolean;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
/** Verified classification outcome for an addressed memory location. */
|
||||
export interface HardwareMemoryClassification {
|
||||
readonly canonicalAddress: string;
|
||||
readonly addressValue: bigint;
|
||||
readonly architecture: HardwareArchitecture;
|
||||
readonly regionKind: HardwareMemoryRegionKind;
|
||||
readonly regionName: string;
|
||||
readonly range: {
|
||||
readonly start: string;
|
||||
readonly end: string;
|
||||
};
|
||||
readonly isMmio: boolean;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
const HEX_ADDRESS_PATTERN = /^0x[0-9a-fA-F]{1,16}$/u;
|
||||
|
||||
const formatHex = (value: bigint): string => `0x${value.toString(16)}`;
|
||||
|
||||
const maxAddressFor = (architecture: HardwareArchitecture): bigint =>
|
||||
architecture === "arm64" ? 0xffffffffffffffffn : 0xffffffffn;
|
||||
|
||||
// Standard ARM Cortex-M / ARMv7-M / ARMv8-M memory architecture.
|
||||
const ARM_REGIONS: readonly HardwareMemoryRegionDefinition[] = [
|
||||
{
|
||||
name: "Code / Flash",
|
||||
kind: "code",
|
||||
start: 0x00000000n,
|
||||
end: 0x1fffffffn,
|
||||
isMmio: false,
|
||||
description: "Executable Flash ROM, BootROM, or vector table mapping",
|
||||
},
|
||||
{
|
||||
name: "Internal SRAM",
|
||||
kind: "sram",
|
||||
start: 0x20000000n,
|
||||
end: 0x3fffffffn,
|
||||
isMmio: false,
|
||||
description: "Internal SRAM, stack, heap, and static data allocation",
|
||||
},
|
||||
{
|
||||
name: "Peripheral MMIO",
|
||||
kind: "peripheral_mmio",
|
||||
start: 0x40000000n,
|
||||
end: 0x5fffffffn,
|
||||
isMmio: true,
|
||||
description:
|
||||
"Microcontroller peripherals: GPIO, UART, SPI, I2C, Timers, ADC, DMA",
|
||||
},
|
||||
{
|
||||
name: "External RAM",
|
||||
kind: "external_device",
|
||||
start: 0x60000000n,
|
||||
end: 0x9fffffffn,
|
||||
isMmio: false,
|
||||
description: "External RAM accessed via FMC/FSMC; non-peripheral memory",
|
||||
},
|
||||
{
|
||||
name: "External Device Space",
|
||||
kind: "external_device",
|
||||
start: 0xa0000000n,
|
||||
end: 0xdfffffffn,
|
||||
isMmio: false,
|
||||
description: "External device memory or external storage mapping",
|
||||
},
|
||||
{
|
||||
name: "System Control Space (SCS)",
|
||||
kind: "system_control",
|
||||
start: 0xe0000000n,
|
||||
end: 0xe00fffffn,
|
||||
isMmio: true,
|
||||
description: "ARM core system control: NVIC, SysTick, SCB, MPU, DWT, ITM",
|
||||
},
|
||||
{
|
||||
name: "Reserved Architecture Space",
|
||||
kind: "reserved",
|
||||
start: 0xe0100000n,
|
||||
end: 0xffffffffn,
|
||||
isMmio: false,
|
||||
description: "Vendor or architecture reserved space",
|
||||
},
|
||||
];
|
||||
|
||||
// Standard embedded RISC-V platform memory architecture.
|
||||
const RISCV_REGIONS: readonly HardwareMemoryRegionDefinition[] = [
|
||||
{
|
||||
name: "Debug & Boot ROM",
|
||||
kind: "code",
|
||||
start: 0x00000000n,
|
||||
end: 0x01ffffffn,
|
||||
isMmio: false,
|
||||
description: "On-chip Debug module, mask ROM, and boot execution space",
|
||||
},
|
||||
{
|
||||
name: "Core Local Interruptor (CLINT)",
|
||||
kind: "system_control",
|
||||
start: 0x02000000n,
|
||||
end: 0x0bffffffn,
|
||||
isMmio: true,
|
||||
description: "RISC-V timer and software interrupt control registers",
|
||||
},
|
||||
{
|
||||
name: "Platform-Level Interrupt Controller (PLIC)",
|
||||
kind: "system_control",
|
||||
start: 0x0c000000n,
|
||||
end: 0x0fffffffn,
|
||||
isMmio: true,
|
||||
description:
|
||||
"RISC-V external interrupt controller priority and enable registers",
|
||||
},
|
||||
{
|
||||
name: "Peripheral MMIO",
|
||||
kind: "peripheral_mmio",
|
||||
start: 0x10000000n,
|
||||
end: 0x1fffffffn,
|
||||
isMmio: true,
|
||||
description: "SoC peripheral registers: UART, SPI, GPIO, PWM, I2C",
|
||||
},
|
||||
{
|
||||
name: "Flash Memory",
|
||||
kind: "code",
|
||||
start: 0x20000000n,
|
||||
end: 0x7fffffffn,
|
||||
isMmio: false,
|
||||
description: "SPI/QSPI XIP Flash executable code",
|
||||
},
|
||||
{
|
||||
name: "Main System RAM",
|
||||
kind: "sram",
|
||||
start: 0x80000000n,
|
||||
end: 0xffffffffn,
|
||||
isMmio: false,
|
||||
description: "Main SRAM or DDR memory space",
|
||||
},
|
||||
];
|
||||
|
||||
// Standard 32-bit MIPS memory segments.
|
||||
const MIPS_REGIONS: readonly HardwareMemoryRegionDefinition[] = [
|
||||
{
|
||||
name: "kuseg (User TLB Space)",
|
||||
kind: "code",
|
||||
start: 0x00000000n,
|
||||
end: 0x7fffffffn,
|
||||
isMmio: false,
|
||||
description: "User virtual space, TLB-mapped and cached",
|
||||
},
|
||||
{
|
||||
name: "kseg0 (Kernel Unmapped Cached)",
|
||||
kind: "sram",
|
||||
start: 0x80000000n,
|
||||
end: 0x9fffffffn,
|
||||
isMmio: false,
|
||||
description: "Kernel executable and data space, cached and unmapped",
|
||||
},
|
||||
{
|
||||
name: "kseg1 (Kernel Unmapped Uncached)",
|
||||
kind: "sram",
|
||||
start: 0xa0000000n,
|
||||
end: 0xbfbfffffn,
|
||||
isMmio: false,
|
||||
description:
|
||||
"Kernel uncached space: mirrors physical memory for DMA and non-cached buffers",
|
||||
},
|
||||
{
|
||||
name: "kseg1 Boot ROM / Reset Vector",
|
||||
kind: "code",
|
||||
start: 0xbfc00000n,
|
||||
end: 0xbfffffffn,
|
||||
isMmio: false,
|
||||
description: "Reset vector and firmware boot ROM in uncached kernel space",
|
||||
},
|
||||
{
|
||||
name: "kseg2 / kseg3 (Kernel TLB Space)",
|
||||
kind: "system_control",
|
||||
start: 0xc0000000n,
|
||||
end: 0xffffffffn,
|
||||
isMmio: false,
|
||||
description: "Kernel mapped space and debug/supervisor control",
|
||||
},
|
||||
];
|
||||
|
||||
// Standard x86 hardware memory map.
|
||||
const X86_REGIONS: readonly HardwareMemoryRegionDefinition[] = [
|
||||
{
|
||||
name: "Real Mode Base RAM",
|
||||
kind: "sram",
|
||||
start: 0x00000000n,
|
||||
end: 0x0009ffffn,
|
||||
isMmio: false,
|
||||
description: "Conventional real-mode RAM and interrupt vector table",
|
||||
},
|
||||
{
|
||||
name: "Video MMIO (VGA)",
|
||||
kind: "peripheral_mmio",
|
||||
start: 0x000a0000n,
|
||||
end: 0x000bffffn,
|
||||
isMmio: true,
|
||||
description: "Legacy VGA text and graphics frame buffer MMIO",
|
||||
},
|
||||
{
|
||||
name: "Option ROM / BIOS Code",
|
||||
kind: "code",
|
||||
start: 0x000c0000n,
|
||||
end: 0x000fffffn,
|
||||
isMmio: false,
|
||||
description: "Expansion ROM and legacy BIOS firmware code",
|
||||
},
|
||||
{
|
||||
name: "System RAM",
|
||||
kind: "sram",
|
||||
start: 0x00100000n,
|
||||
end: 0xfebfffffn,
|
||||
isMmio: false,
|
||||
description: "Extended physical memory space",
|
||||
},
|
||||
{
|
||||
name: "APIC System Control MMIO",
|
||||
kind: "system_control",
|
||||
start: 0xfec00000n,
|
||||
end: 0xfeefffffn,
|
||||
isMmio: true,
|
||||
description: "I/O APIC and Local APIC memory-mapped control registers",
|
||||
},
|
||||
{
|
||||
name: "BIOS Firmware Flash",
|
||||
kind: "code",
|
||||
start: 0xfff00000n,
|
||||
end: 0xffffffffn,
|
||||
isMmio: false,
|
||||
description:
|
||||
"System firmware ROM containing x86 reset vector at 0xfffffff0",
|
||||
},
|
||||
];
|
||||
|
||||
// Generic 64-bit ARM64 / AArch64 hardware layout.
|
||||
const ARM64_REGIONS: readonly HardwareMemoryRegionDefinition[] = [
|
||||
{
|
||||
name: "Flash / Firmware Code",
|
||||
kind: "code",
|
||||
start: 0x0000000000000000n,
|
||||
end: 0x0000000007ffffffn,
|
||||
isMmio: false,
|
||||
description: "Primary system firmware and boot code",
|
||||
},
|
||||
{
|
||||
name: "Interrupt Controller (GIC) MMIO",
|
||||
kind: "system_control",
|
||||
start: 0x0000000008000000n,
|
||||
end: 0x0000000008ffffffn,
|
||||
isMmio: true,
|
||||
description:
|
||||
"ARM Generic Interrupt Controller (GIC) distributor and CPU interfaces",
|
||||
},
|
||||
{
|
||||
name: "Peripheral MMIO Space",
|
||||
kind: "peripheral_mmio",
|
||||
start: 0x0000000009000000n,
|
||||
end: 0x000000003fffffffn,
|
||||
isMmio: true,
|
||||
description: "Memory-mapped I/O peripherals and PCIe configuration ECAM",
|
||||
},
|
||||
{
|
||||
name: "System RAM / DRAM",
|
||||
kind: "sram",
|
||||
start: 0x0000000040000000n,
|
||||
end: 0x000000ffffffffffn,
|
||||
isMmio: false,
|
||||
description: "Main physical memory space",
|
||||
},
|
||||
{
|
||||
name: "Extended Hardware Space",
|
||||
kind: "reserved",
|
||||
start: 0x0000010000000000n,
|
||||
end: 0xffffffffffffffffn,
|
||||
isMmio: false,
|
||||
description: "Upper 64-bit physical address space",
|
||||
},
|
||||
];
|
||||
|
||||
const getArchitectureRegions = (
|
||||
arch: HardwareArchitecture,
|
||||
): readonly HardwareMemoryRegionDefinition[] => {
|
||||
switch (arch) {
|
||||
case "arm":
|
||||
return ARM_REGIONS;
|
||||
case "riscv":
|
||||
return RISCV_REGIONS;
|
||||
case "mips":
|
||||
return MIPS_REGIONS;
|
||||
case "x86":
|
||||
return X86_REGIONS;
|
||||
case "arm64":
|
||||
return ARM64_REGIONS;
|
||||
}
|
||||
};
|
||||
|
||||
/** Parse and canonicalize an address input into a safe BigInt representation. */
|
||||
export const parseAddressValue = (
|
||||
address: bigint | string,
|
||||
): Result<bigint, string> => {
|
||||
if (typeof address === "bigint") {
|
||||
return address < 0n
|
||||
? err("Address cannot be a negative value")
|
||||
: ok(address);
|
||||
}
|
||||
if (typeof address !== "string" || !HEX_ADDRESS_PATTERN.test(address)) {
|
||||
return err("Address must be a hexadecimal string of the form 0x…");
|
||||
}
|
||||
try {
|
||||
const parsed = BigInt(address);
|
||||
return ok(parsed);
|
||||
} catch {
|
||||
return err("Invalid hexadecimal address format");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify a target address within the standardized hardware memory map for
|
||||
* the specified CPU architecture. Distinguishes code, SRAM, peripheral MMIO,
|
||||
* external devices, and core system control spaces.
|
||||
*/
|
||||
export const classifyHardwareMemoryAddress = (
|
||||
address: bigint | string,
|
||||
architecture: HardwareArchitecture,
|
||||
): Result<HardwareMemoryClassification, string> => {
|
||||
const parsed = parseAddressValue(address);
|
||||
if (!parsed.ok) return parsed;
|
||||
const value = parsed.value;
|
||||
const max = maxAddressFor(architecture);
|
||||
if (value > max) {
|
||||
return err(
|
||||
`Address ${formatHex(value)} exceeds ${architecture} maximum address width ${formatHex(max)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const regions = getArchitectureRegions(architecture);
|
||||
const matched = regions.find(
|
||||
(region) => value >= region.start && value <= region.end,
|
||||
);
|
||||
|
||||
if (matched !== undefined) {
|
||||
return ok({
|
||||
canonicalAddress: formatHex(value),
|
||||
addressValue: value,
|
||||
architecture,
|
||||
regionKind: matched.kind,
|
||||
regionName: matched.name,
|
||||
range: {
|
||||
start: formatHex(matched.start),
|
||||
end: formatHex(matched.end),
|
||||
},
|
||||
isMmio: matched.isMmio,
|
||||
description: matched.description,
|
||||
});
|
||||
}
|
||||
|
||||
return ok({
|
||||
canonicalAddress: formatHex(value),
|
||||
addressValue: value,
|
||||
architecture,
|
||||
regionKind: "reserved",
|
||||
regionName: "Unknown / Unmapped Region",
|
||||
range: { start: formatHex(value), end: formatHex(value) },
|
||||
isMmio: false,
|
||||
description: "Address does not match any recognized hardware region",
|
||||
});
|
||||
};
|
||||
|
||||
/** Fast boolean predicate to check if an address represents memory-mapped I/O. */
|
||||
export const isHardwareMmio = (
|
||||
address: bigint | string,
|
||||
architecture: HardwareArchitecture,
|
||||
): boolean => {
|
||||
const result = classifyHardwareMemoryAddress(address, architecture);
|
||||
return result.ok ? result.value.isMmio : false;
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
attributeHardwareAccess,
|
||||
summarizeHardwarePeripheralInteractions,
|
||||
type AnalyzedInstructionReference,
|
||||
type HardwareAccessAttribution,
|
||||
} from "./hardwarePeripheralAttribution.js";
|
||||
|
||||
describe("Hardware peripheral reference attribution", () => {
|
||||
it("attributes a read access to a UART peripheral register", () => {
|
||||
const reference: AnalyzedInstructionReference = {
|
||||
target_address: "0x40011004",
|
||||
read: true,
|
||||
write: false,
|
||||
source_address: "0x08000150",
|
||||
operand_index: 1,
|
||||
provenance: "instruction_disassembly",
|
||||
};
|
||||
|
||||
const result = attributeHardwareAccess(reference, "arm");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
expect(result.value).toEqual({
|
||||
targetAddress: "0x40011004",
|
||||
addressValue: 0x40011004n,
|
||||
architecture: "arm",
|
||||
regionKind: "peripheral_mmio",
|
||||
regionName: "Peripheral MMIO",
|
||||
isMmio: true,
|
||||
accessKind: "read",
|
||||
peripheralBlockBase: "0x40011000",
|
||||
peripheralBlockOffset: "0x4",
|
||||
sourceAddress: "0x08000150",
|
||||
operandIndex: 1,
|
||||
provenance: "instruction_disassembly",
|
||||
});
|
||||
});
|
||||
|
||||
it("attributes a write access to a GPIO peripheral register", () => {
|
||||
const reference: AnalyzedInstructionReference = {
|
||||
target_address: "0x40020014",
|
||||
read: false,
|
||||
write: true,
|
||||
source_address: "0x08000180",
|
||||
operand_index: 0,
|
||||
};
|
||||
|
||||
const result = attributeHardwareAccess(reference, "arm");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
expect(result.value.accessKind).toBe("write");
|
||||
expect(result.value.isMmio).toBe(true);
|
||||
expect(result.value.peripheralBlockBase).toBe("0x40020000");
|
||||
expect(result.value.peripheralBlockOffset).toBe("0x14");
|
||||
});
|
||||
|
||||
it("attributes a read/write access to ARM System Control Space (SCS)", () => {
|
||||
const reference: AnalyzedInstructionReference = {
|
||||
target_address: "0xe000e010",
|
||||
read: true,
|
||||
write: true,
|
||||
source_address: "0x08000200",
|
||||
};
|
||||
|
||||
const result = attributeHardwareAccess(reference, "arm");
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
expect(result.value.regionKind).toBe("system_control");
|
||||
expect(result.value.accessKind).toBe("read_write");
|
||||
expect(result.value.isMmio).toBe(true);
|
||||
expect(result.value.peripheralBlockBase).toBe("0xe000e000");
|
||||
expect(result.value.peripheralBlockOffset).toBe("0x10");
|
||||
});
|
||||
|
||||
it("distinguishes non-MMIO code and RAM memory accesses", () => {
|
||||
const codeRef: AnalyzedInstructionReference = {
|
||||
target_address: "0x08000500",
|
||||
call: true,
|
||||
};
|
||||
const codeResult = attributeHardwareAccess(codeRef, "arm");
|
||||
expect(codeResult.ok).toBe(true);
|
||||
if (!codeResult.ok) return;
|
||||
expect(codeResult.value.isMmio).toBe(false);
|
||||
expect(codeResult.value.regionKind).toBe("code");
|
||||
|
||||
const ramRef: AnalyzedInstructionReference = {
|
||||
target_address: "0x20000400",
|
||||
read: true,
|
||||
};
|
||||
const ramResult = attributeHardwareAccess(ramRef, "arm");
|
||||
expect(ramResult.ok).toBe(true);
|
||||
if (!ramResult.ok) return;
|
||||
expect(ramResult.value.isMmio).toBe(false);
|
||||
expect(ramResult.value.regionKind).toBe("sram");
|
||||
});
|
||||
|
||||
it("rejects invalid target address representations", () => {
|
||||
const invalid: AnalyzedInstructionReference = {
|
||||
target_address: "malformed",
|
||||
};
|
||||
const result = attributeHardwareAccess(invalid, "arm");
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toContain("hexadecimal string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Hardware peripheral interaction summarization", () => {
|
||||
it("aggregates MMIO accesses into distinct peripheral blocks", () => {
|
||||
const references: AnalyzedInstructionReference[] = [
|
||||
// Block 0x40011000 (UART1): 2 writes, 1 read
|
||||
{ target_address: "0x40011000", write: true },
|
||||
{ target_address: "0x40011004", read: true },
|
||||
{ target_address: "0x40011004", write: true },
|
||||
// Block 0x40020000 (GPIOA): 1 write
|
||||
{ target_address: "0x40020014", write: true },
|
||||
// Non-MMIO accesses: Code and Stack RAM
|
||||
{ target_address: "0x08000100", call: true },
|
||||
{ target_address: "0x20000000", read: true },
|
||||
];
|
||||
|
||||
const attributions: HardwareAccessAttribution[] = [];
|
||||
for (const ref of references) {
|
||||
const attributed = attributeHardwareAccess(ref, "arm");
|
||||
if (attributed.ok) attributions.push(attributed.value);
|
||||
}
|
||||
|
||||
const summary = summarizeHardwarePeripheralInteractions(
|
||||
attributions,
|
||||
"arm",
|
||||
);
|
||||
|
||||
expect(summary.totalReferencesExamined).toBe(6);
|
||||
expect(summary.mmioAccessCount).toBe(4);
|
||||
expect(summary.nonMmioAccessCount).toBe(2);
|
||||
expect(summary.peripheralBlocks).toHaveLength(2);
|
||||
|
||||
const uartBlock = summary.peripheralBlocks[0];
|
||||
expect(uartBlock).toBeDefined();
|
||||
if (uartBlock === undefined) return;
|
||||
expect(uartBlock.blockBase).toBe("0x40011000");
|
||||
expect(uartBlock.regionKind).toBe("peripheral_mmio");
|
||||
expect(uartBlock.totalAccesses).toBe(3);
|
||||
expect(uartBlock.readCount).toBe(1);
|
||||
expect(uartBlock.writeCount).toBe(2);
|
||||
expect(uartBlock.uniqueAddresses).toEqual(["0x40011000", "0x40011004"]);
|
||||
expect(uartBlock.observedOffsets).toEqual(["0x0", "0x4"]);
|
||||
|
||||
const gpioBlock = summary.peripheralBlocks[1];
|
||||
expect(gpioBlock).toBeDefined();
|
||||
if (gpioBlock === undefined) return;
|
||||
expect(gpioBlock.blockBase).toBe("0x40020000");
|
||||
expect(gpioBlock.totalAccesses).toBe(1);
|
||||
expect(gpioBlock.writeCount).toBe(1);
|
||||
expect(gpioBlock.readCount).toBe(0);
|
||||
expect(gpioBlock.observedOffsets).toEqual(["0x14"]);
|
||||
});
|
||||
|
||||
it("handles empty attribution sets without error", () => {
|
||||
const emptySummary = summarizeHardwarePeripheralInteractions([], "arm");
|
||||
expect(emptySummary.totalReferencesExamined).toBe(0);
|
||||
expect(emptySummary.mmioAccessCount).toBe(0);
|
||||
expect(emptySummary.peripheralBlocks).toEqual([]);
|
||||
});
|
||||
|
||||
it("sorts observed register offsets numerically rather than lexicographically", () => {
|
||||
const references: AnalyzedInstructionReference[] = [
|
||||
{ target_address: "0x40011100", write: true },
|
||||
{ target_address: "0x40011004", read: true },
|
||||
{ target_address: "0x40011010", write: true },
|
||||
];
|
||||
|
||||
const attributions: HardwareAccessAttribution[] = [];
|
||||
for (const ref of references) {
|
||||
const attributed = attributeHardwareAccess(ref, "arm");
|
||||
if (attributed.ok) attributions.push(attributed.value);
|
||||
}
|
||||
|
||||
const summary = summarizeHardwarePeripheralInteractions(
|
||||
attributions,
|
||||
"arm",
|
||||
);
|
||||
|
||||
expect(summary.peripheralBlocks).toHaveLength(1);
|
||||
const block = summary.peripheralBlocks[0];
|
||||
expect(block).toBeDefined();
|
||||
if (block === undefined) return;
|
||||
expect(block.observedOffsets).toEqual(["0x4", "0x10", "0x100"]);
|
||||
expect(block.uniqueAddresses).toEqual([
|
||||
"0x40011004",
|
||||
"0x40011010",
|
||||
"0x40011100",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
classifyHardwareMemoryAddress,
|
||||
type HardwareArchitecture,
|
||||
type HardwareMemoryRegionKind,
|
||||
} from "./hardwareMemoryMap.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
|
||||
/** Minimal input reference contract representing an analyzed instruction reference. */
|
||||
export interface AnalyzedInstructionReference {
|
||||
readonly target_address: string;
|
||||
readonly type?: string;
|
||||
readonly call?: boolean;
|
||||
readonly jump?: boolean;
|
||||
readonly data?: boolean | null;
|
||||
readonly read?: boolean | null;
|
||||
readonly write?: boolean | null;
|
||||
readonly source_address?: string | null;
|
||||
readonly operand_index?: number;
|
||||
readonly provenance?: string | null;
|
||||
}
|
||||
|
||||
/** Access direction observed from the instruction reference. */
|
||||
export type HardwareAccessKind = "read" | "write" | "read_write" | "unknown";
|
||||
|
||||
/** Structured hardware peripheral attribution for an analyzed instruction reference. */
|
||||
export interface HardwareAccessAttribution {
|
||||
readonly targetAddress: string;
|
||||
readonly addressValue: bigint;
|
||||
readonly architecture: HardwareArchitecture;
|
||||
readonly regionKind: HardwareMemoryRegionKind;
|
||||
readonly regionName: string;
|
||||
readonly isMmio: boolean;
|
||||
readonly accessKind: HardwareAccessKind;
|
||||
readonly peripheralBlockBase: string;
|
||||
readonly peripheralBlockOffset: string;
|
||||
readonly sourceAddress?: string | null;
|
||||
readonly operandIndex?: number | null;
|
||||
readonly provenance?: string | null;
|
||||
}
|
||||
|
||||
/** Summarized hardware peripheral block with observed register accesses. */
|
||||
export interface HardwarePeripheralBlockSummary {
|
||||
readonly blockBase: string;
|
||||
readonly regionName: string;
|
||||
readonly regionKind: HardwareMemoryRegionKind;
|
||||
readonly totalAccesses: number;
|
||||
readonly readCount: number;
|
||||
readonly writeCount: number;
|
||||
readonly uniqueAddresses: readonly string[];
|
||||
readonly observedOffsets: readonly string[];
|
||||
}
|
||||
|
||||
/** Complete aggregated summary of hardware peripheral interactions. */
|
||||
export interface HardwarePeripheralAttributionSummary {
|
||||
readonly architecture: HardwareArchitecture;
|
||||
readonly totalReferencesExamined: number;
|
||||
readonly mmioAccessCount: number;
|
||||
readonly nonMmioAccessCount: number;
|
||||
readonly peripheralBlocks: readonly HardwarePeripheralBlockSummary[];
|
||||
}
|
||||
|
||||
const deriveAccessKind = (
|
||||
read?: boolean | null,
|
||||
write?: boolean | null,
|
||||
): HardwareAccessKind => {
|
||||
if (read === true && write === true) return "read_write";
|
||||
if (write === true) return "write";
|
||||
if (read === true) return "read";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
/**
|
||||
* Attribute an analyzed instruction reference to its underlying hardware memory
|
||||
* region, calculating peripheral block coordinates and access direction.
|
||||
*/
|
||||
export const attributeHardwareAccess = (
|
||||
reference: AnalyzedInstructionReference,
|
||||
architecture: HardwareArchitecture,
|
||||
): Result<HardwareAccessAttribution, string> => {
|
||||
const classification = classifyHardwareMemoryAddress(
|
||||
reference.target_address,
|
||||
architecture,
|
||||
);
|
||||
if (!classification.ok) return classification;
|
||||
|
||||
const { canonicalAddress, addressValue, regionKind, regionName, isMmio } =
|
||||
classification.value;
|
||||
|
||||
// Group into standard 4 KiB (0x1000) hardware peripheral register pages.
|
||||
const blockBaseValue = addressValue & ~0xfffn;
|
||||
const blockOffsetValue = addressValue & 0xfffn;
|
||||
|
||||
const accessKind = deriveAccessKind(reference.read, reference.write);
|
||||
|
||||
return ok({
|
||||
targetAddress: canonicalAddress,
|
||||
addressValue,
|
||||
architecture,
|
||||
regionKind,
|
||||
regionName,
|
||||
isMmio,
|
||||
accessKind,
|
||||
peripheralBlockBase: `0x${blockBaseValue.toString(16)}`,
|
||||
peripheralBlockOffset: `0x${blockOffsetValue.toString(16)}`,
|
||||
sourceAddress: reference.source_address ?? null,
|
||||
operandIndex: reference.operand_index ?? null,
|
||||
provenance: reference.provenance ?? null,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggregate and group multiple hardware memory attributions into cohesive
|
||||
* peripheral block summaries. Filters out non-MMIO memory (e.g. code/stack RAM)
|
||||
* to provide a clear reconstruction picture of hardware peripherals touched by firmware.
|
||||
*/
|
||||
export const summarizeHardwarePeripheralInteractions = (
|
||||
attributions: readonly HardwareAccessAttribution[],
|
||||
architecture: HardwareArchitecture,
|
||||
): HardwarePeripheralAttributionSummary => {
|
||||
let mmioAccessCount = 0;
|
||||
let nonMmioAccessCount = 0;
|
||||
|
||||
type BlockAccumulator = {
|
||||
readonly blockBase: string;
|
||||
readonly regionName: string;
|
||||
readonly regionKind: HardwareMemoryRegionKind;
|
||||
totalAccesses: number;
|
||||
readCount: number;
|
||||
writeCount: number;
|
||||
readonly uniqueAddresses: Set<string>;
|
||||
readonly observedOffsets: Set<string>;
|
||||
};
|
||||
|
||||
const blocks = new Map<string, BlockAccumulator>();
|
||||
|
||||
for (const item of attributions) {
|
||||
if (!item.isMmio) {
|
||||
nonMmioAccessCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
mmioAccessCount += 1;
|
||||
let block = blocks.get(item.peripheralBlockBase);
|
||||
if (block === undefined) {
|
||||
block = {
|
||||
blockBase: item.peripheralBlockBase,
|
||||
regionName: item.regionName,
|
||||
regionKind: item.regionKind,
|
||||
totalAccesses: 0,
|
||||
readCount: 0,
|
||||
writeCount: 0,
|
||||
uniqueAddresses: new Set<string>(),
|
||||
observedOffsets: new Set<string>(),
|
||||
};
|
||||
blocks.set(item.peripheralBlockBase, block);
|
||||
}
|
||||
|
||||
block.totalAccesses += 1;
|
||||
if (item.accessKind === "read" || item.accessKind === "read_write") {
|
||||
block.readCount += 1;
|
||||
}
|
||||
if (item.accessKind === "write" || item.accessKind === "read_write") {
|
||||
block.writeCount += 1;
|
||||
}
|
||||
block.uniqueAddresses.add(item.targetAddress);
|
||||
block.observedOffsets.add(item.peripheralBlockOffset);
|
||||
}
|
||||
|
||||
const peripheralBlocks: HardwarePeripheralBlockSummary[] = Array.from(
|
||||
blocks.values(),
|
||||
)
|
||||
.sort((a, b) => (BigInt(a.blockBase) < BigInt(b.blockBase) ? -1 : 1))
|
||||
.map((block) => ({
|
||||
blockBase: block.blockBase,
|
||||
regionName: block.regionName,
|
||||
regionKind: block.regionKind,
|
||||
totalAccesses: block.totalAccesses,
|
||||
readCount: block.readCount,
|
||||
writeCount: block.writeCount,
|
||||
uniqueAddresses: Array.from(block.uniqueAddresses).sort((a, b) =>
|
||||
BigInt(a) < BigInt(b) ? -1 : 1,
|
||||
),
|
||||
observedOffsets: Array.from(block.observedOffsets).sort((a, b) =>
|
||||
BigInt(a) < BigInt(b) ? -1 : 1,
|
||||
),
|
||||
}));
|
||||
|
||||
return {
|
||||
architecture,
|
||||
totalReferencesExamined: attributions.length,
|
||||
mmioAccessCount,
|
||||
nonMmioAccessCount,
|
||||
peripheralBlocks,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -961,3 +962,119 @@ describe("complete Ghidra function body range evidence", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra native instruction decoder context for ARM TMode and MIPS ISA_MODE", () => {
|
||||
it.each([
|
||||
{
|
||||
arch: "ARM:LE:32:v7",
|
||||
register: "TMode",
|
||||
value: "1",
|
||||
description: "Thumb execution mode",
|
||||
},
|
||||
{
|
||||
arch: "ARM:LE:32:v7",
|
||||
register: "TMode",
|
||||
value: "0",
|
||||
description: "ARM 32-bit execution mode",
|
||||
},
|
||||
{
|
||||
arch: "MIPS:LE:32:default",
|
||||
register: "ISA_MODE",
|
||||
value: "1",
|
||||
description: "MIPS16 mode",
|
||||
},
|
||||
{
|
||||
arch: "MIPS:LE:32:default",
|
||||
register: "ISA_MODE",
|
||||
value: "0",
|
||||
description: "Standard MIPS32 mode",
|
||||
},
|
||||
])(
|
||||
"preserves $description ($register=$value) across boundary",
|
||||
({ arch, register, value }) => {
|
||||
const instruction = {
|
||||
...compactInstruction(),
|
||||
architecture: arch,
|
||||
decoder_context: [
|
||||
{
|
||||
register,
|
||||
value,
|
||||
source: "ghidra-program-context",
|
||||
},
|
||||
],
|
||||
};
|
||||
const parsed = parseFunctionValueFixture(
|
||||
"inspect_native_instruction",
|
||||
instruction,
|
||||
);
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) return;
|
||||
expect(parsed.value).toMatchObject({
|
||||
architecture: arch,
|
||||
decoder_context: [
|
||||
{ register, value, source: "ghidra-program-context" },
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("admits null value for uninitialized processor context register", () => {
|
||||
const instruction = {
|
||||
...compactInstruction(),
|
||||
architecture: "ARM:LE:32:v7",
|
||||
decoder_context: [
|
||||
{
|
||||
register: "TMode",
|
||||
value: null,
|
||||
source: "ghidra-program-context",
|
||||
},
|
||||
],
|
||||
};
|
||||
const parsed = parseFunctionValueFixture(
|
||||
"inspect_native_instruction",
|
||||
instruction,
|
||||
);
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) return;
|
||||
expect(parsed.value).toMatchObject({
|
||||
decoder_context: [{ register: "TMode", value: null }],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed decoder context entries", () => {
|
||||
const emptyRegister = {
|
||||
...compactInstruction(),
|
||||
decoder_context: [{ register: "", value: "1", source: "ghidra" }],
|
||||
};
|
||||
expect(
|
||||
parseFunctionValueFixture("inspect_native_instruction", emptyRegister).ok,
|
||||
).toBe(false);
|
||||
|
||||
const emptySource = {
|
||||
...compactInstruction(),
|
||||
decoder_context: [{ register: "TMode", value: "1", source: "" }],
|
||||
};
|
||||
expect(
|
||||
parseFunctionValueFixture("inspect_native_instruction", emptySource).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra bridge script processor context contract", () => {
|
||||
it("binds TMode to ARM and ISA_MODE to MIPS in ReaGhidraBridge.java", async () => {
|
||||
const bridgePath = new URL(
|
||||
"../../bridge/ghidra/ReaGhidraBridge.java",
|
||||
import.meta.url,
|
||||
);
|
||||
const content = await readFile(bridgePath, "utf8");
|
||||
expect(content).toContain(
|
||||
'currentProgram.getLanguageID().getIdAsString().startsWith("ARM:")',
|
||||
);
|
||||
expect(content).toContain(
|
||||
'currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")',
|
||||
);
|
||||
expect(content).toContain('"TMode"');
|
||||
expect(content).toContain('"ISA_MODE"');
|
||||
expect(content).toContain('"ghidra-program-context"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ const MIPS_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
`;
|
||||
|
||||
const pe: BinaryTarget = {
|
||||
path: "/fixture.exe",
|
||||
path: process.platform === "win32" ? "C:\\fixture.exe" : "/fixture.exe",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
|
||||
@@ -102,4 +102,45 @@ describe("compact Ghidra reference MCP evidence", () => {
|
||||
await harness.close();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("preserves ARM TMode and Thumb decoder context across the MCP boundary", async () => {
|
||||
const instructionWithContext = (value: string | null) => ({
|
||||
...compactInstruction(),
|
||||
architecture: "ARM:LE:32:v7",
|
||||
decoder_context: [
|
||||
{
|
||||
register: "TMode",
|
||||
value,
|
||||
source: "ghidra-program-context",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
for (const tmodeValue of ["0", "1", null]) {
|
||||
const outputs = {
|
||||
inspect_native_instruction: instructionWithContext(tmodeValue),
|
||||
};
|
||||
const harness = await connectGhidraMcp(
|
||||
`ghidra-arm-tmode-${String(tmodeValue)}`,
|
||||
(operation) => {
|
||||
if (operation !== "inspect_native_instruction")
|
||||
throw new TypeError(`Unexpected operation: ${operation}`);
|
||||
return Promise.resolve(ok(outputs[operation]));
|
||||
},
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: "0x08000000" },
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
expect(
|
||||
sessionEvidence(harness.session, result.structuredContent)
|
||||
.normalized_result,
|
||||
).toEqual(outputs.inspect_native_instruction);
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//@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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user