Files
rea/tests/conformance/ghidra/mips-fixture.mjs
T
Aurelio Bernal 5e219de441 feat(ghidra): admit soft-float MIPS32r2 ELF with integer-register float storage (#1949)
* feat(ghidra): admit soft-float MIPS32r2 ELF with integer-register float storage

Soft-float o32 executables (Linux kernel builds, bare-metal firmware) were
refused because their ABI flags record declares no CPR1 and FP ABI 3.
Ghidra's MIPS32 compiler specifications model only hard-float o32, so typed
float/double signatures (DWARF) would be bound to $f12/$f14/$f0 and the
decompiler loses the real dataflow: `return sel ? a : b` decompiled to
`return in_f0;`, and callers passed phantom FPU inputs.

Admit ELF32 ET_EXEC o32 MIPS32r2 targets whose ABI record declares soft-float
with no CPR1, and commit `mips_soft_float_storage: o32-integer-registers-v1`
in their analysis profile only (hard-float profile digests are unchanged).
ReaGhidraMipsSoftFloat.java runs after auto-analysis and, for each function
with a float/double formal type, either keeps the types with the storage
Ghidra's o32 model assigns to a same-size integer (pair alignment, stack
spill), or, for auto-parameters such as hidden struct-return pointers, swaps
float types for same-size integers under dynamic storage. No function is left
on hard-float storage. The report is written last; the provider client
requires it exactly when the profile commits the policy and refuses the
session before any operation otherwise.

Verification: real Ghidra 12.1.4 lane (verify:ghidra:mips) now builds
hard- and soft-float fixtures in both byte orders and checks exact
parameter/return storage through inspect_native_api, caller constants,
snapshot profile and Evidence outcome; test:fast, verify:package and an
installed-tarball soft-float run pass.

* fix(ghidra): model soft-float MIPS as a prototype for every typed callable

Review feedback on #1949: rewriting storage per Function left external
functions and indirect calls through typed function pointers on Ghidra's
hard-float o32 prototype, which still read $f12/$f14/$f0 while the session
reported success. A negative control confirms it: without the change,
`(*rea_mips_soft_callback)(8.5f, 9.5f, sel)` decompiles to
`(*rea_mips_soft_callback)(in_f12, in_f14, sel)`.

ReaGhidraMipsSoftFloat.java now derives a `__rea_soft_float` prototype from
the program's own default o32 model (encoded, float pentries and
float-consuming rules removed, so byte-order-specific aggregate rules and the
hidden struct-return pointer are kept), installs it as a specification
extension, and assigns it with dynamic storage to every non-thunk function,
external function and function-pointer definition whose formal types use
float or double. Thunks follow their targets. Float types are kept
everywhere, so the integer-retype fallback is removed. The script fails
without writing its report if the derived prototype or any reassigned
function still uses an FPU register.

The policy becomes `o32-soft-float-prototype-v1` with a
{functions, definitions} report. The soft-float fixture adds an indirect
call through a typed pointer; the real-Ghidra lane requires its constants
with no FPU inputs and the struct return's `float scale` in $a1.

* test(ghidra): pin ODDSPREG acceptance for soft-float MIPS ABI records

Review on #1949 suggested requiring flags1 == 0 for soft-float, since MIPS_AFL_FLAGS1_ODDSPREG names CPR1 registers. GCC 5.2 kernel modules, LLVM/Rust firmware and Clang -msoft-float objects all record flags1 = 1 with FP ABI soft-float: it is the MIPS32 o32 toolchain default and selects nothing without CPR1. Keep accepting it, say why at the check, and test that soft-float with ODDSPREG is admitted while any other general flag is refused.
2026-10-12 04:54:38 +08:00

95 lines
2.8 KiB
JavaScript

import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const execute = promisify(execFile);
/** Build a source-owned freestanding ELF; never execute its target code. */
export async function buildMipsFixture(
directory,
byteOrder,
floatAbi = "hard",
) {
if (byteOrder !== "little" && byteOrder !== "big")
throw new Error("MIPS fixture byte order must be little or big");
if (floatAbi !== "hard" && floatAbi !== "soft")
throw new Error("MIPS fixture float ABI must be hard or soft");
const soft = floatAbi === "soft";
const compiler = process.env.REA_MIPS_CLANG ?? "clang";
const target = byteOrder === "little" ? "mipsel-linux-gnu" : "mips-linux-gnu";
const source = fileURLToPath(new URL("../c/mips.c", import.meta.url));
const softSource = fileURLToPath(
new URL("../c/mips-soft-float.c", import.meta.url),
);
const probe = fileURLToPath(new URL("./mips-probe.S", import.meta.url));
const path = join(directory, `${target}${soft ? "-soft-float" : ""}.elf`);
const args = [
`--target=${target}`,
"-march=mips32r2",
"-mabi=32",
...(soft ? ["-msoft-float"] : []),
"-mno-abicalls",
"-fno-pic",
"-fuse-ld=lld",
"-nostdlib",
"-static",
"-O0",
"-g",
"-Wl,-e,rea_mips_entry",
"-Wl,--build-id=none",
source,
...(soft ? [softSource] : []),
probe,
"-o",
path,
];
const version = await execute(compiler, ["--version"], { timeout: 10000 });
await execute(compiler, args, { timeout: 60000 });
const bytes = await readFile(path);
const read16 =
byteOrder === "little"
? bytes.readUInt16LE.bind(bytes)
: bytes.readUInt16BE.bind(bytes);
const read32 =
byteOrder === "little"
? bytes.readUInt32LE.bind(bytes)
: bytes.readUInt32BE.bind(bytes);
if (
bytes.length < 52 ||
bytes.toString("hex", 0, 4) !== "7f454c46" ||
bytes[4] !== 1 ||
bytes[5] !== (byteOrder === "little" ? 1 : 2) ||
read16(16) !== 2 ||
read16(18) !== 8 ||
read32(36) !== 0x70001001
)
throw new Error(
"Compiler did not produce the declared ELF32/o32/MIPS32r2 fixture",
);
return {
path,
byte_order: byteOrder,
float_abi: floatAbi,
flags: read32(36),
sha256: createHash("sha256").update(bytes).digest("hex"),
source_sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
...(soft
? {
soft_source_sha256: createHash("sha256")
.update(await readFile(softSource))
.digest("hex"),
}
: {}),
probe_sha256: createHash("sha256")
.update(await readFile(probe))
.digest("hex"),
compiler: version.stdout.trim(),
arguments: args,
};
}