mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
c86b09a109
* feat(ghidra): admit 32-bit big-endian PowerPC ELF Recognize EM_PPC as a provider-neutral `powerpc` family that keeps its ELF class, byte order, type and flags, and admit ELF32 big-endian ET_EXEC through a bounded Ghidra lane. ELF64, little-endian, relocatable/shared objects and EF_PPC_EMB are refused with a specific reason; Hopper and IDA do not gain PowerPC support, and Windows Ghidra P0 is unchanged. Ghidra's ELF loader selects PowerPC:BE:32:e500:default for a plain EM_PPC header, which misreads 750-family code. The lane binds PowerPC:BE:32:default/default in the profile (language_selection "admitted-lane-v1"), imports with it and checks the reported language. A language set through REA_GHIDRA_LANGUAGE_ID still replaces the lane default. Add a source-owned fixture, fixed-encoding probe, GNU readelf cross-check and the optional `verify:ghidra:ppc` real-provider lane, plus docs/ghidra-ppc.md. Refs #1741 * style(ghidra): format shared PowerPC profile integration * fix(ghidra): preserve caller-selected PowerPC languages --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
const execute = promisify(execFile);
|
|
// The lane binds this language; Ghidra's ELF loader alone selects e500.
|
|
const LANGUAGE = "PowerPC:BE:32:default";
|
|
const wordHex = (value) => {
|
|
const bytes = Buffer.alloc(4);
|
|
bytes.writeUInt32BE(value);
|
|
return bytes.toString("hex");
|
|
};
|
|
|
|
/** Parse only the documented GNU readelf fields needed by our tiny fixture. */
|
|
export function parsePowerPcReadelf(text) {
|
|
const field = (expression, label) => {
|
|
const matches = [...text.matchAll(expression)];
|
|
assert.equal(matches.length, 1, `Missing or ambiguous readelf ${label}`);
|
|
return matches[0][1].trim();
|
|
};
|
|
assert.equal(field(/^\s*Class:\s*(.+)$/gmu, "class"), "ELF32");
|
|
assert.equal(
|
|
field(/^\s*Data:.*?,\s*(little|big) endian\s*$/gmu, "byte order"),
|
|
"big",
|
|
);
|
|
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
|
|
assert.equal(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), "PowerPC");
|
|
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+)\s*$/gmu, "flags"));
|
|
assert.equal(flags, 0, "Fixture ELF flags differ from build intent");
|
|
const symbols = {};
|
|
for (const name of [
|
|
"rea_ppc_entry",
|
|
"rea_ppc_leaf",
|
|
"rea_ppc_probe",
|
|
"rea_ppc_global",
|
|
"rea_ppc_marker",
|
|
]) {
|
|
const value = field(
|
|
new RegExp(
|
|
`^\\s*\\d+:\\s+([\\da-f]+)\\s+\\d+\\s+(?:FUNC|OBJECT)\\s+GLOBAL\\s+\\S+\\s+\\d+\\s+${name}\\s*$`,
|
|
"gmu",
|
|
),
|
|
`symbol ${name}`,
|
|
);
|
|
symbols[name] = `0x${BigInt(`0x${value}`).toString(16)}`;
|
|
}
|
|
return {
|
|
powerpc: { elfClass: 32, byteOrder: "big", type: 2, flags },
|
|
symbols,
|
|
};
|
|
}
|
|
|
|
/** Independent reader is a bounded test prerequisite, never an REA provider. */
|
|
export async function inspectPowerPcReadelf(path) {
|
|
const command = process.env.REA_PPC_READELF ?? "readelf";
|
|
const options = {
|
|
env: { ...process.env, LC_ALL: "C" },
|
|
timeout: 10000,
|
|
maxBuffer: 1024 * 1024,
|
|
};
|
|
const version = await execute(command, ["--version"], options);
|
|
assert.match(
|
|
version.stdout,
|
|
/^GNU readelf\b/u,
|
|
"This lane requires GNU readelf",
|
|
);
|
|
const result = await execute(command, ["-h", "-s", "-W", path], options);
|
|
assert.equal(
|
|
result.stderr.trim(),
|
|
"",
|
|
`readelf diagnostics: ${result.stderr}`,
|
|
);
|
|
return {
|
|
...parsePowerPcReadelf(result.stdout),
|
|
command,
|
|
version: version.stdout.split("\n")[0],
|
|
raw: result.stdout,
|
|
};
|
|
}
|
|
|
|
/** Check effective Ghidra observations, not the requested analysis profile. */
|
|
export function assertPowerPcLoadedImage(observations, sha256) {
|
|
assert.equal(observations.language_id, LANGUAGE);
|
|
assert.equal(observations.compiler_spec_id, "default");
|
|
assert.ok(
|
|
observations.source_files.some(
|
|
(file) =>
|
|
file.original_sha256 === sha256 && file.modified_sha256 === sha256,
|
|
),
|
|
"Ghidra loaded source identity changed",
|
|
);
|
|
}
|
|
|
|
/** Verify the exact constant/conditional destination in ppc-probe.S. */
|
|
export function assertPowerPcProbe(move, branch, address) {
|
|
const start = BigInt(address);
|
|
for (const [instruction, offset, word] of [
|
|
[move, 0n, 0x38641234],
|
|
[branch, 8n, 0x41820008],
|
|
]) {
|
|
assert.equal(instruction.status, "decoded");
|
|
assert.equal(BigInt(instruction.address), start + offset);
|
|
assert.equal(instruction.architecture, LANGUAGE);
|
|
assert.equal(instruction.length, 4);
|
|
assert.equal(instruction.bytes, wordHex(word));
|
|
}
|
|
assert.match(move.mnemonic, /^ADDI$/iu);
|
|
assert.ok(
|
|
move.operands.some((operand) =>
|
|
operand.components.some(
|
|
(part) =>
|
|
part.kind === "immediate" &&
|
|
part.value !== null &&
|
|
BigInt(part.value) === 0x1234n,
|
|
),
|
|
),
|
|
"ADDI immediate was not recovered",
|
|
);
|
|
assert.match(branch.mnemonic, /^BEQ$/iu);
|
|
assert.equal(branch.flow.kind, "jump");
|
|
assert.equal(branch.flow.conditional, true);
|
|
assert.equal(branch.flow.computed, false);
|
|
assert.deepEqual(
|
|
branch.flow.direct_destinations.map((value) => BigInt(value)),
|
|
[start + 16n],
|
|
);
|
|
}
|
|
|
|
/** Four bytes must encode the known source-owned global in big-endian order. */
|
|
export function assertPowerPcGlobal(bytes) {
|
|
assert.equal(bytes, wordHex(7));
|
|
}
|