mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
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.
This commit is contained in:
@@ -450,6 +450,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
String seeds = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA analysis seeds", null);
|
||||
if (seeds != null) result.add("analysis_seeds", JsonParser.parseString(seeds));
|
||||
// Written by ReaGhidraMipsSoftFloat after analysis; REA requires it
|
||||
// exactly when the committed profile declares soft-float storage.
|
||||
String softFloat = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA MIPS soft-float storage", null);
|
||||
if (softFloat != null) result.add("mips_soft_float_storage", JsonParser.parseString(softFloat));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.database.SpecExtension;
|
||||
import ghidra.program.model.data.AbstractFloatDataType;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.data.DoubleDataType;
|
||||
import ghidra.program.model.data.FloatDataType;
|
||||
import ghidra.program.model.data.FunctionDefinition;
|
||||
import ghidra.program.model.data.IntegerDataType;
|
||||
import ghidra.program.model.data.ParameterDefinition;
|
||||
import ghidra.program.model.data.TypeDef;
|
||||
import ghidra.program.model.lang.CompilerSpec;
|
||||
import ghidra.program.model.lang.PrototypeModel;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.listing.VariableStorage;
|
||||
import ghidra.program.model.pcode.XmlEncode;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
/**
|
||||
* Soft-float o32 passes and returns float/double values exactly where same-size integers go.
|
||||
* Ghidra's MIPS32 compiler specifications model only hard-float o32, so every typed float
|
||||
* prototype (functions, externals, thunks via their targets, and function-pointer definitions)
|
||||
* would otherwise read $f12/$f14/$f0. Derive a program-specific prototype model from the
|
||||
* program's own default o32 model with its floating-point entries and rules removed, install it
|
||||
* as a specification extension, and assign it to every callable whose formal types use floats.
|
||||
*/
|
||||
public final class ReaGhidraMipsSoftFloat extends GhidraScript {
|
||||
static final String REPORT_KEY = "REA MIPS soft-float storage";
|
||||
static final String POLICY = "o32-soft-float-prototype-v1";
|
||||
static final String PROTOTYPE = "__rea_soft_float";
|
||||
private static final Pattern FPU_REGISTER = Pattern.compile("f\\d+(_\\d+)?");
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String processor = currentProgram.getLanguage().getProcessor().toString();
|
||||
if (!processor.equals("MIPS") || currentProgram.getDefaultPointerSize() != 4)
|
||||
throw new IllegalStateException(
|
||||
"REA soft-float storage requires a 32-bit MIPS program, not " + processor);
|
||||
CompilerSpec spec = currentProgram.getCompilerSpec();
|
||||
PrototypeModel base = spec.getDefaultCallingConvention();
|
||||
new SpecExtension(currentProgram)
|
||||
.addReplaceCompilerSpecExtension(softFloatPrototype(base, spec), monitor);
|
||||
PrototypeModel soft = currentProgram.getCompilerSpec().getCallingConvention(PROTOTYPE);
|
||||
if (soft == null)
|
||||
throw new IllegalStateException("REA soft-float prototype was not installed");
|
||||
requireIntegerStorage(
|
||||
soft.getStorageLocations(
|
||||
currentProgram,
|
||||
new DataType[] {
|
||||
FloatDataType.dataType, IntegerDataType.dataType, DoubleDataType.dataType,
|
||||
FloatDataType.dataType, DoubleDataType.dataType
|
||||
},
|
||||
false),
|
||||
"soft-float prototype self-check");
|
||||
int functions = 0;
|
||||
List<Function> callables = new ArrayList<>();
|
||||
currentProgram.getFunctionManager().getFunctions(true).forEach(callables::add);
|
||||
currentProgram.getFunctionManager().getExternalFunctions().forEach(callables::add);
|
||||
for (Function function : callables) {
|
||||
// A thunk's signature is its target's; the target is in this list.
|
||||
if (function.isThunk() || !usesFloat(function)) continue;
|
||||
if (function.hasCustomVariableStorage()) function.setCustomVariableStorage(false);
|
||||
function.setCallingConvention(PROTOTYPE);
|
||||
List<VariableStorage> storage = new ArrayList<>();
|
||||
storage.add(function.getReturn().getVariableStorage());
|
||||
for (Parameter parameter : function.getParameters()) storage.add(parameter.getVariableStorage());
|
||||
requireIntegerStorage(storage.toArray(VariableStorage[]::new), function.getName(true));
|
||||
functions += 1;
|
||||
}
|
||||
int definitions = 0;
|
||||
Iterator<DataType> types = currentProgram.getDataTypeManager().getAllDataTypes();
|
||||
while (types.hasNext()) {
|
||||
if (!(types.next() instanceof FunctionDefinition definition) || !usesFloat(definition))
|
||||
continue;
|
||||
definition.setCallingConvention(PROTOTYPE);
|
||||
definitions += 1;
|
||||
}
|
||||
// Written last: any failure above leaves no report, and REA refuses the session.
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setString(
|
||||
REPORT_KEY,
|
||||
"{\"policy\":\"" + POLICY + "\",\"functions\":" + functions +
|
||||
",\"definitions\":" + definitions + "}");
|
||||
println("REA MIPS soft-float prototype: functions=" + functions + " definitions=" + definitions);
|
||||
}
|
||||
|
||||
/** The default o32 model without floating-point registers or float-consuming rules. */
|
||||
private static String softFloatPrototype(PrototypeModel base, CompilerSpec spec) throws Exception {
|
||||
XmlEncode encoder = new XmlEncode();
|
||||
base.encode(encoder, spec.getPcodeInjectLibrary());
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
Document document =
|
||||
factory.newDocumentBuilder().parse(new InputSource(new StringReader(encoder.toString())));
|
||||
Element root = document.getDocumentElement();
|
||||
if (!root.getTagName().equals("prototype"))
|
||||
throw new IllegalStateException("Unexpected default prototype encoding " + root.getTagName());
|
||||
root.setAttribute("name", PROTOTYPE);
|
||||
List<Node> removed = new ArrayList<>();
|
||||
collect(root.getElementsByTagName("pentry"), "metatype", "float", null, removed);
|
||||
collect(root.getElementsByTagName("consume"), "storage", "float", "rule", removed);
|
||||
if (removed.isEmpty())
|
||||
throw new IllegalStateException("Default prototype has no floating-point entries to remove");
|
||||
for (Node node : removed) node.getParentNode().removeChild(node);
|
||||
StringWriter xml = new StringWriter();
|
||||
var transformer = TransformerFactory.newInstance().newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
transformer.transform(new DOMSource(document), new StreamResult(xml));
|
||||
return xml.toString();
|
||||
}
|
||||
|
||||
/** Select matching elements, or their nearest `ancestor` element when one is named. */
|
||||
private static void collect(
|
||||
NodeList nodes, String attribute, String value, String ancestor, List<Node> selected) {
|
||||
for (int index = 0; index < nodes.getLength(); index += 1) {
|
||||
Element element = (Element) nodes.item(index);
|
||||
if (!value.equals(element.getAttribute(attribute))) continue;
|
||||
Node target = element;
|
||||
while (ancestor != null && target != null && !ancestor.equals(target.getNodeName()))
|
||||
target = target.getParentNode();
|
||||
if (target != null && !selected.contains(target)) selected.add(target);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireIntegerStorage(VariableStorage[] storage, String owner) {
|
||||
for (VariableStorage item : storage) {
|
||||
if (item.isBadStorage() || item.isUnassignedStorage())
|
||||
throw new IllegalStateException("REA soft-float storage unresolved for " + owner);
|
||||
List<Register> registers = item.getRegisters();
|
||||
if (registers == null) continue; // stack-only storage
|
||||
for (Register register : registers) {
|
||||
if (FPU_REGISTER.matcher(register.getName()).matches())
|
||||
throw new IllegalStateException(
|
||||
"REA soft-float storage still uses " + register.getName() + " for " + owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean usesFloat(Function function) {
|
||||
if (isFloat(function.getReturn().getFormalDataType())) return true;
|
||||
for (Parameter parameter : function.getParameters()) {
|
||||
if (isFloat(parameter.getFormalDataType())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean usesFloat(FunctionDefinition definition) {
|
||||
if (isFloat(definition.getReturnType())) return true;
|
||||
for (ParameterDefinition argument : definition.getArguments()) {
|
||||
if (isFloat(argument.getDataType())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isFloat(DataType type) {
|
||||
DataType current = type;
|
||||
while (current instanceof TypeDef typeDef) current = typeDef.getBaseDataType();
|
||||
return current instanceof AbstractFloatDataType;
|
||||
}
|
||||
}
|
||||
+49
-8
@@ -39,12 +39,14 @@ absent or supply a default ABI.
|
||||
The field layout follows
|
||||
[`Elf_MIPS_ABIFlags_v0`](https://github.com/bminor/glibc/blob/master/elf/elf.h).
|
||||
|
||||
Provider admission requires record version 0, MIPS32r2, 32-bit GPR/CPR1, no CPR2,
|
||||
and a double-precision or FPXX ABI declaration with no ISA extension, ASE or
|
||||
unknown general flags. Missing records remain unknown rather than defaulting
|
||||
to a compatible ABI, and this initial lane refuses them. Unknown record values
|
||||
are retained for diagnosis but do not gain provider support. These checks bind
|
||||
the declared interpretation; the integer-only fixture does not establish full
|
||||
Provider admission requires record version 0, MIPS32r2, 32-bit GPRs, no CPR2,
|
||||
no ISA extension, ASE or unknown general flags, and one of two floating-point
|
||||
declarations: 32-bit CPR1 with a double-precision or FPXX ABI, or no CPR1 with
|
||||
the soft-float ABI. Other combinations, including single-float, FP64 and
|
||||
unspecified ABIs, are refused. Missing records remain unknown rather than
|
||||
defaulting to a compatible ABI, and this lane refuses them. Unknown record
|
||||
values are retained for diagnosis but do not gain provider support. These checks
|
||||
bind the declared interpretation; the fixtures do not establish full
|
||||
floating-point behavioral conformance.
|
||||
|
||||
MIPS header and ABI record facts are committed in the analysis profile so
|
||||
@@ -53,6 +55,37 @@ identity is versioned independently of file identity. Evidence, lifecycle
|
||||
outputs and saved snapshots preserve the `mips` family. Hopper and IDA adapters
|
||||
do not implicitly gain MIPS support. Windows Ghidra P0 remains PE x86/x86-64 only.
|
||||
|
||||
### Soft-float prototype
|
||||
|
||||
Ghidra's MIPS32 compiler specifications model hard-float o32 only: a typed
|
||||
`float` or `double` parameter is bound to `$f12`/`$f14` and returned in `$f0`.
|
||||
Soft-float o32 passes those values in integer registers and stack slots, so a
|
||||
typed prototype (for example from DWARF) would make the decompiler read FPU
|
||||
registers the code never writes and drop the real dataflow. That applies to
|
||||
functions, external functions and indirect calls through typed function
|
||||
pointers alike.
|
||||
|
||||
For soft-float targets REA commits
|
||||
`mips_soft_float_storage: o32-soft-float-prototype-v1` in the analysis profile
|
||||
and runs `ReaGhidraMipsSoftFloat.java` after auto-analysis. The script encodes
|
||||
the program's own default o32 prototype, removes its floating-point parameter
|
||||
and return entries and the rules that consume floating-point storage, and
|
||||
installs the result as the program-specific `__rea_soft_float` prototype through
|
||||
a Ghidra specification extension. Deriving it keeps the byte-order-specific
|
||||
aggregate rules and the hidden struct-return pointer. The script then assigns
|
||||
that prototype, with dynamic storage, to every non-thunk function and external
|
||||
function, and to every function-pointer definition, whose declared types use
|
||||
`float` or `double`. Thunks follow their targets. Types are kept.
|
||||
|
||||
The script fails, and writes no report, if the derived prototype still places a
|
||||
float in an FPU register or any reassigned function does. It records the
|
||||
function and definition counts last; REA requires that report exactly when the
|
||||
profile commits the policy and refuses the session otherwise, before serving any
|
||||
operation. Session Evidence carries the policy and counts as limitations.
|
||||
Callables without typed float signatures keep Ghidra's default prototype, under
|
||||
which Ghidra already infers integer-register parameters. Hard-float profiles do
|
||||
not gain the policy, so their digests are unchanged.
|
||||
|
||||
## Use
|
||||
|
||||
With a caller-installed Ghidra and compatible JDK on a supported Linux/macOS host:
|
||||
@@ -70,7 +103,8 @@ REA's ELF parser.
|
||||
|
||||
## Verification
|
||||
|
||||
The source-owned freestanding fixture is `tests/conformance/c/mips.c`. The
|
||||
The source-owned freestanding fixture is `tests/conformance/c/mips.c`; the
|
||||
soft-float variant also links `tests/conformance/c/mips-soft-float.c`. The
|
||||
optional cross-target lane requires a caller-supplied Clang with MIPS targets,
|
||||
LLD, GNU `readelf`, Ghidra and its compatible JDK; ordinary host-native checks
|
||||
do not acquire these tools. `REA_MIPS_CLANG` selects Clang and
|
||||
@@ -81,7 +115,8 @@ These are test-time prerequisites, not REA runtime providers.
|
||||
GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra npm run verify:ghidra:mips
|
||||
```
|
||||
|
||||
The lane compiles both byte orders and never executes the targets. GNU readelf
|
||||
The lane compiles both byte orders with hard-float and soft-float ABIs and never
|
||||
executes the targets. GNU readelf
|
||||
independently supplies header/ABI facts and symbol addresses, compared with the
|
||||
production REA target resolver before provider startup. Its version, bounded
|
||||
command output and selected tool path are retained in the verification report.
|
||||
@@ -104,6 +139,12 @@ all program-header payloads. It checks the actual Ghidra language, entry-address
|
||||
decompilation, distinct artifact digest and missing-section limitations through
|
||||
CLI Evidence and an MCP-produced snapshot. Caller-selected Ghidra heap limits
|
||||
are passed to these child workflows.
|
||||
For soft-float fixtures the lane also checks, through `inspect_native_api`, the
|
||||
exact parameter and return types and storage of float, double, mixed (pair
|
||||
alignment and stack spill) and struct-returning functions; that a caller's
|
||||
decompilation receives every source float constant without FPU inputs,
|
||||
including an indirect call through a typed function pointer; and that the
|
||||
snapshot profile and Evidence retain the policy and outcome counts.
|
||||
Pseudocode presence is a liveness check, not proof of semantic equivalence.
|
||||
Instruction decoding and reader agreement likewise do not establish runtime,
|
||||
delay-slot execution or complete floating-point semantics. Assertion regressions
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"bridge/ghidra/ReaGhidraApplySeeds.java",
|
||||
"bridge/ghidra/ReaGhidraAnalysisOptions.java",
|
||||
"bridge/ghidra/ReaGhidraNoReturnFix.java",
|
||||
"bridge/ghidra/ReaGhidraMipsSoftFloat.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareRaw.java",
|
||||
"bridge/android/ReaJadxBridge.java",
|
||||
"bridge/mitmproxy/capture.py",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
assertMipsLoadedImage,
|
||||
assertMipsProbe,
|
||||
assertMipsGlobal,
|
||||
assertMipsSoftFloatStorage,
|
||||
} from "../tests/conformance/ghidra/mips-checks.mjs";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
|
||||
@@ -77,16 +78,64 @@ const env = {
|
||||
for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
|
||||
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed float signatures must use soft-float o32 storage, and callers must see
|
||||
* the source constants through it rather than FPU inputs.
|
||||
*/
|
||||
async function verifySoftFloat(call, symbols, byteOrder) {
|
||||
const boundaries = {};
|
||||
for (const name of [
|
||||
"rea_mips_soft_pick",
|
||||
"rea_mips_soft_wide",
|
||||
"rea_mips_soft_mixed",
|
||||
"rea_mips_soft_struct",
|
||||
]) {
|
||||
const api = await call("inspect_native_api", { procedure: symbols[name] });
|
||||
boundaries[name] = api.boundary;
|
||||
}
|
||||
assertMipsSoftFloatStorage(boundaries, byteOrder);
|
||||
const caller = functionDossierSchema.parse(
|
||||
await call("analyze_function", {
|
||||
procedure: symbols.rea_mips_soft_caller,
|
||||
}),
|
||||
);
|
||||
// Includes the struct call and the indirect call through a typed pointer.
|
||||
assert.doesNotMatch(caller.pseudocode, /\bin_f\d+\b/u);
|
||||
for (const literal of [
|
||||
"1.5",
|
||||
"2.5",
|
||||
"3.25",
|
||||
"4.75",
|
||||
"5.5",
|
||||
"6.125",
|
||||
"7.5",
|
||||
"8.5",
|
||||
"9.5",
|
||||
])
|
||||
assert.ok(
|
||||
caller.pseudocode.includes(literal),
|
||||
`Soft-float caller lost argument ${literal}`,
|
||||
);
|
||||
return { storage: boundaries };
|
||||
}
|
||||
const reports = [];
|
||||
let primaryFailure;
|
||||
try {
|
||||
for (const byteOrder of psp ? ["little"] : ["little", "big"]) {
|
||||
for (const [byteOrder, floatAbi] of psp
|
||||
? [["little", "hard"]]
|
||||
: [
|
||||
["little", "hard"],
|
||||
["big", "hard"],
|
||||
["little", "soft"],
|
||||
["big", "soft"],
|
||||
]) {
|
||||
const fixture = psp
|
||||
? await buildPspFixture(workspace)
|
||||
: await buildMipsFixture(workspace, byteOrder);
|
||||
: await buildMipsFixture(workspace, byteOrder, floatAbi);
|
||||
const independent = psp
|
||||
? fixture
|
||||
: await inspectMipsReadelf(fixture.path, byteOrder);
|
||||
: await inspectMipsReadelf(fixture.path, byteOrder, floatAbi);
|
||||
const resolved = await parseBinaryTarget(fixture.path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
if (psp) {
|
||||
@@ -104,7 +153,10 @@ try {
|
||||
);
|
||||
}
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const snapshotPath = join(workspace, `${byteOrder}.snapshot.json`);
|
||||
const snapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}.snapshot.json`,
|
||||
);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
@@ -238,6 +290,10 @@ try {
|
||||
callees.includes(leaf.address),
|
||||
"Expected direct call not recovered",
|
||||
);
|
||||
const softFloat =
|
||||
floatAbi === "soft"
|
||||
? await verifySoftFloat(call, independent.symbols, byteOrder)
|
||||
: undefined;
|
||||
const assembly = await call("procedure_assembly", {
|
||||
procedure: entry.address,
|
||||
});
|
||||
@@ -293,6 +349,19 @@ try {
|
||||
},
|
||||
"Snapshot profile did not retain the independently inspected ELF/ABI declaration",
|
||||
);
|
||||
assert.equal(
|
||||
snapshot.binding.analysis_profile.parameters.mips_soft_float_storage,
|
||||
floatAbi === "soft" ? "o32-soft-float-prototype-v1" : undefined,
|
||||
);
|
||||
assert.equal(
|
||||
snapshot.evidence_bundle.records.some((record) =>
|
||||
/Soft-float prototype outcome: 4 functions and [1-9]\d* function-pointer definitions use __rea_soft_float\./u.test(
|
||||
JSON.stringify(record),
|
||||
),
|
||||
),
|
||||
floatAbi === "soft",
|
||||
"Soft-float prototype outcome missing from, or unexpected in, retained Evidence",
|
||||
);
|
||||
assert.ok(snapshot.evidence_bundle.records.length > 0);
|
||||
if (psp) {
|
||||
assert.equal(
|
||||
@@ -419,7 +488,10 @@ try {
|
||||
assert.ok(read32(offset + 4) + read32(offset + 16) <= cut);
|
||||
}
|
||||
const carvedBytes = sourceBytes.subarray(0, cut);
|
||||
const carvedPath = join(workspace, `${byteOrder}-carved.elf`);
|
||||
const carvedPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}-carved.elf`,
|
||||
);
|
||||
const carvedSha256 = createHash("sha256")
|
||||
.update(carvedBytes)
|
||||
.digest("hex");
|
||||
@@ -460,7 +532,7 @@ try {
|
||||
assert.ok(carvedDossier.pseudocode.trim().length > 0);
|
||||
const carvedSnapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-carved.snapshot.json`,
|
||||
`${byteOrder}-${floatAbi}-carved.snapshot.json`,
|
||||
);
|
||||
await call("close_binary", { snapshot_path: carvedSnapshotPath });
|
||||
const carvedSnapshot = parseAnalysisSnapshot(
|
||||
@@ -523,7 +595,11 @@ try {
|
||||
: "conditional branch destination",
|
||||
"global word and marker bytes",
|
||||
"CLI/MCP and snapshot identity",
|
||||
...(floatAbi === "soft"
|
||||
? ["soft-float o32 parameter/return storage and caller constants"]
|
||||
: []),
|
||||
],
|
||||
...(softFloat === undefined ? {} : { soft_float: softFloat }),
|
||||
procedures: procedures.length,
|
||||
carved: carved ?? {
|
||||
status: "not_run",
|
||||
@@ -534,7 +610,7 @@ try {
|
||||
});
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`MIPS ${byteOrder} verification failed; MCP stderr: ${stderr}`,
|
||||
`MIPS ${byteOrder} ${floatAbi}-float verification failed; MCP stderr: ${stderr}`,
|
||||
{ cause },
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -114,6 +114,8 @@ export interface GhidraHeadlessLauncherOptions {
|
||||
readonly analysisSeeds?: GhidraSeedCommitment;
|
||||
readonly analysisExtensions?: readonly GhidraExtension[];
|
||||
readonly pspExtension?: GhidraPspExtension;
|
||||
/** Remap typed float storage for the soft-float o32 ABI after analysis. */
|
||||
readonly mipsSoftFloat?: true;
|
||||
/** Spawn seam for provider-boundary lifecycle tests. */
|
||||
readonly spawnProcess?: typeof spawnOwnedProviderProcess;
|
||||
}
|
||||
@@ -196,6 +198,9 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
const headlessArguments = ghidraHeadlessArguments({
|
||||
platform,
|
||||
...(psp === undefined ? {} : { psp: true as const }),
|
||||
...(this.options.mipsSoftFloat === undefined
|
||||
? {}
|
||||
: { mipsSoftFloat: this.options.mipsSoftFloat }),
|
||||
projectRoot: paths.projectRoot,
|
||||
targetPath: session.targetPath,
|
||||
bridgeScriptPath: this.options.bridgeScriptPath,
|
||||
@@ -422,6 +427,7 @@ export interface GhidraHeadlessArgumentOptions {
|
||||
readonly ghidraLogPath: string;
|
||||
readonly scriptLogPath: string;
|
||||
readonly psp?: true;
|
||||
readonly mipsSoftFloat?: true;
|
||||
readonly dosMz?: true;
|
||||
readonly dosCom?: true;
|
||||
readonly rawBinary?: {
|
||||
@@ -460,6 +466,16 @@ export const ghidraHeadlessArguments = (
|
||||
throw new Error(
|
||||
"A PSP ELF import cannot be combined with a raw-binary import.",
|
||||
);
|
||||
if (
|
||||
options.mipsSoftFloat === true &&
|
||||
(options.psp === true ||
|
||||
options.dosMz === true ||
|
||||
options.dosCom === true ||
|
||||
rawBinary !== undefined)
|
||||
)
|
||||
throw new Error(
|
||||
"MIPS soft-float storage applies only to a generic MIPS ELF import.",
|
||||
);
|
||||
if (
|
||||
override !== undefined &&
|
||||
(options.dosMz === true || options.dosCom === true)
|
||||
@@ -578,6 +594,13 @@ export const ghidraHeadlessArguments = (
|
||||
"-postScript",
|
||||
scriptPath.join(bridgeDirectory, "ReaGhidraNoReturnFix.java"),
|
||||
]),
|
||||
// After analysis and the no-return refit, before the bridge serves results.
|
||||
...(options.mipsSoftFloat === true
|
||||
? [
|
||||
"-postScript",
|
||||
scriptPath.join(bridgeDirectory, "ReaGhidraMipsSoftFloat.java"),
|
||||
]
|
||||
: []),
|
||||
"-postScript",
|
||||
// Ghidra checks the caller's cwd before scriptPath for a basename. Select
|
||||
// the packaged source explicitly so unrelated entries cannot shadow it.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ok } from "../domain/result.js";
|
||||
import { parseConfig } from "../config/parseConfig.js";
|
||||
import type {
|
||||
BinaryTarget,
|
||||
@@ -10,6 +11,8 @@ import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
|
||||
import { IdaProvider } from "../ida/IdaProvider.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js";
|
||||
|
||||
// Fake installation inspection tests admission/profile contracts, not Ghidra.
|
||||
const host: GhidraInstallationHost = {
|
||||
@@ -56,6 +59,18 @@ const target = (change: Partial<MipsElfMetadata> = {}): BinaryTarget => ({
|
||||
...change,
|
||||
},
|
||||
});
|
||||
const abi = (
|
||||
change: Partial<NonNullable<MipsElfMetadata["abiFlags"]>>,
|
||||
): Partial<MipsElfMetadata> => {
|
||||
const base = target();
|
||||
if (base.kind !== "executable" || base.architecture !== "mips")
|
||||
throw new Error("expected a MIPS fixture target");
|
||||
const flags = base.mips.abiFlags;
|
||||
if (flags === undefined || flags === null)
|
||||
throw new Error("expected fixture ABI flags");
|
||||
return { abiFlags: { ...flags, ...change } };
|
||||
};
|
||||
const softFloat = abi({ cpr1Size: 0, fpAbi: 3 });
|
||||
|
||||
describe("bounded Ghidra MIPS admission", () => {
|
||||
it("binds endian and flags without widening Windows P0", async () => {
|
||||
@@ -118,6 +133,11 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
[{ flags: 0x70001201 }, "EF_MIPS_FP64"],
|
||||
[{ flags: 0x70001401 }, "EF_MIPS_NAN2008"],
|
||||
[{ abiFlags: null }, "inspected ABI"],
|
||||
[abi({ cpr1Size: 0, fpAbi: 1 }), "soft-float without CPR1"],
|
||||
[abi({ cpr1Size: 1, fpAbi: 3 }), "soft-float without CPR1"],
|
||||
[abi({ cpr1Size: 1, fpAbi: 2 }), "single-float"],
|
||||
[abi({ cpr1Size: 1, fpAbi: 0 }), "unspecified"],
|
||||
[abi({ cpr1Size: 2, fpAbi: 6 }), "FP64"],
|
||||
] satisfies [Partial<MipsElfMetadata>, string][])(
|
||||
"refuses an unverified MIPS interpretation %j",
|
||||
async (change, reason) => {
|
||||
@@ -140,3 +160,136 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
expect(ida.inspectTargetSupport(target()).status).toBe("unsupported");
|
||||
});
|
||||
});
|
||||
|
||||
describe("soft-float MIPS storage", () => {
|
||||
const report: GhidraMipsSoftFloatReport = {
|
||||
policy: "o32-soft-float-prototype-v1",
|
||||
functions: 5,
|
||||
definitions: 1,
|
||||
};
|
||||
const session = async (
|
||||
value: BinaryTarget,
|
||||
reported: GhidraMipsSoftFloatReport | undefined,
|
||||
profileFor: BinaryTarget = value,
|
||||
) => {
|
||||
const counts = { starts: 0, calls: 0, closes: 0 };
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host, () => ({
|
||||
start: () => {
|
||||
counts.starts++;
|
||||
return Promise.resolve(
|
||||
ok({
|
||||
name: "REA Ghidra bridge" as const,
|
||||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: "a".repeat(64),
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: false,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
|
||||
target: {
|
||||
name: "fixture.elf",
|
||||
language_id: "MIPS:LE:32:default",
|
||||
compiler_spec_id: "default",
|
||||
image_base: "0x0",
|
||||
default_address_space: "ram",
|
||||
sha256: "a".repeat(64),
|
||||
},
|
||||
...(reported === undefined
|
||||
? {}
|
||||
: { mips_soft_float_storage: reported }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
callTool: () => {
|
||||
counts.calls++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
close: () => {
|
||||
counts.closes++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
}));
|
||||
const resolved = await ghidra.resolveAnalysisProfile(profileFor);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a MIPS profile");
|
||||
return {
|
||||
counts,
|
||||
profile: resolved.value.profile,
|
||||
client: ghidra.createClient(value, resolved.value.profile),
|
||||
};
|
||||
};
|
||||
|
||||
it("admits soft-float o32 and commits its storage policy only for it", async () => {
|
||||
const soft = target(softFloat);
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
expect(ghidra.inspectTargetSupport(soft).status).toBe("supported");
|
||||
const { profile } = await session(soft, report);
|
||||
expect(profile.parameters.mips_soft_float_storage).toBe(
|
||||
"o32-soft-float-prototype-v1",
|
||||
);
|
||||
const hard = await session(target(), undefined);
|
||||
expect(hard.profile.parameters).not.toHaveProperty(
|
||||
"mips_soft_float_storage",
|
||||
);
|
||||
});
|
||||
|
||||
it("admits the ODDSPREG default that soft-float toolchains emit, but no other flags", () => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
// GCC 5.2 kernel modules and LLVM/Rust firmware both record flags1 = 1.
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(
|
||||
target(abi({ cpr1Size: 0, fpAbi: 3, flags1: 1 })),
|
||||
).status,
|
||||
).toBe("supported");
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(
|
||||
target(abi({ cpr1Size: 0, fpAbi: 3, flags1: 2 })),
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
reason: expect.stringContaining("general flags"),
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the prototype outcome as session limitations", async () => {
|
||||
const { client } = await session(target(softFloat), report);
|
||||
const health = await client.execute("health", {});
|
||||
if (!health.ok) throw health.error;
|
||||
expect(health.value.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("model hard-float o32 only"),
|
||||
expect.stringContaining(
|
||||
"5 functions and 1 function-pointer definitions use __rea_soft_float.",
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed on a soft-float session without a storage report before any analysis call", async () => {
|
||||
const { counts, client } = await session(target(softFloat), undefined);
|
||||
const failed = await client.execute("list_procedures", {});
|
||||
expect(failed.ok).toBe(false);
|
||||
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
||||
expect(counts).toEqual({ starts: 1, calls: 0, closes: 1 });
|
||||
});
|
||||
|
||||
it("rejects soft-float storage reported by a hard-float session", async () => {
|
||||
const { counts, client } = await session(target(), report);
|
||||
const failed = await client.execute("health", {});
|
||||
expect(failed.ok).toBe(false);
|
||||
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
||||
expect(counts).toEqual({ starts: 1, calls: 0, closes: 1 });
|
||||
});
|
||||
|
||||
it("refuses a profile whose storage policy disagrees with the target", async () => {
|
||||
for (const [value, profileFor] of [
|
||||
[target(softFloat), target()],
|
||||
[target(), target(softFloat)],
|
||||
] as const) {
|
||||
const { counts, client } = await session(value, report, profileFor);
|
||||
const failed = await client.execute("health", {});
|
||||
expect(failed.ok).toBe(false);
|
||||
expect(counts.starts).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
const MIPS_FP_ABI_SOFT = 3;
|
||||
|
||||
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
|
||||
export const ghidraMipsUnsupportedReason = (
|
||||
target: BinaryTarget,
|
||||
@@ -39,17 +41,66 @@ export const ghidraMipsUnsupportedReason = (
|
||||
return "This Ghidra MIPS lane requires ABI flags version 0; newer record semantics are unsupported.";
|
||||
if (abi.isaLevel !== 32 || abi.isaRevision !== 2)
|
||||
return "MIPS ABI flags must agree with the declared MIPS32r2 ISA.";
|
||||
if (abi.gprSize !== 1 || abi.cpr1Size !== 1 || abi.cpr2Size !== 0)
|
||||
return "This Ghidra MIPS lane requires 32-bit GPR/CPR1 and no CPR2; other register modes need separate verification.";
|
||||
if (abi.fpAbi !== 1 && abi.fpAbi !== 5)
|
||||
return "This Ghidra MIPS lane admits double-precision or FPXX ABI declarations only; soft-float, FP64 and unspecified modes need separate verification.";
|
||||
if (abi.gprSize !== 1 || abi.cpr2Size !== 0)
|
||||
return "This Ghidra MIPS lane requires 32-bit GPRs and no CPR2; other register modes need separate verification.";
|
||||
const hardFloat = abi.cpr1Size === 1 && (abi.fpAbi === 1 || abi.fpAbi === 5);
|
||||
const softFloat = abi.cpr1Size === 0 && abi.fpAbi === MIPS_FP_ABI_SOFT;
|
||||
if (!hardFloat && !softFloat)
|
||||
return "This Ghidra MIPS lane admits double-precision or FPXX ABI declarations with 32-bit CPR1, or soft-float without CPR1; single-float, FP64 and unspecified modes need separate verification.";
|
||||
if (abi.isaExtension !== 0 || abi.ases !== 0)
|
||||
return "MIPS ABI flags declare an ISA extension or ASE outside this standard-instruction lane.";
|
||||
// MIPS_AFL_FLAGS1_ODDSPREG (bit 0) is the GCC and LLVM default for MIPS32 o32
|
||||
// whatever the FP ABI; soft-float objects from both carry it. With no CPR1 it
|
||||
// selects nothing, so it is accepted for soft-float as well as hard-float.
|
||||
if ((abi.flags1 & ~1) !== 0 || abi.flags2 !== 0)
|
||||
return "MIPS ABI flags contain unsupported general flags.";
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Soft-float prototype policy applied by ReaGhidraMipsSoftFloat.java after analysis. */
|
||||
export const GHIDRA_MIPS_SOFT_FLOAT_STORAGE = "o32-soft-float-prototype-v1";
|
||||
|
||||
/** Whether the admitted target declares the soft-float o32 ABI. */
|
||||
export const isGhidraMipsSoftFloatTarget = (target: BinaryTarget): boolean =>
|
||||
target.kind === "executable" &&
|
||||
target.architecture === "mips" &&
|
||||
target.mips?.abiFlags?.cpr1Size === 0 &&
|
||||
target.mips.abiFlags.fpAbi === MIPS_FP_ABI_SOFT;
|
||||
|
||||
/** Prototype outcome the post-analysis script records for the bridge handshake. */
|
||||
export interface GhidraMipsSoftFloatReport {
|
||||
readonly policy: typeof GHIDRA_MIPS_SOFT_FLOAT_STORAGE;
|
||||
readonly functions: number;
|
||||
readonly definitions: number;
|
||||
}
|
||||
|
||||
/** Fail closed unless soft-float storage ran exactly for soft-float targets. */
|
||||
export const ghidraMipsSoftFloatFailure = (
|
||||
expected: boolean,
|
||||
report: GhidraMipsSoftFloatReport | undefined,
|
||||
): string | undefined =>
|
||||
expected === (report !== undefined)
|
||||
? undefined
|
||||
: expected
|
||||
? "The Ghidra session did not report the committed MIPS soft-float prototype."
|
||||
: "The Ghidra session reported MIPS soft-float storage that the profile did not commit.";
|
||||
|
||||
/** Keep REA's soft-float prototype distinct from Ghidra's hard-float o32 model. */
|
||||
export const ghidraMipsSoftFloatLimitations = (
|
||||
expected: boolean,
|
||||
report?: GhidraMipsSoftFloatReport,
|
||||
): readonly string[] =>
|
||||
expected
|
||||
? [
|
||||
"Soft-float o32: Ghidra's MIPS32 compiler specifications model hard-float o32 only. After analysis REA installs a program-specific `__rea_soft_float` prototype, derived from the default o32 model without its floating-point registers and rules, and assigns it to every function, external function and function-pointer definition whose declared types use float or double, so those values keep their types and use integer registers and stack slots.",
|
||||
...(report === undefined
|
||||
? []
|
||||
: [
|
||||
`Soft-float prototype outcome: ${report.functions} functions and ${report.definitions} function-pointer definitions use __rea_soft_float.`,
|
||||
]),
|
||||
]
|
||||
: [];
|
||||
|
||||
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
|
||||
export const ghidraMipsProfileParameters = (
|
||||
target: BinaryTarget,
|
||||
@@ -87,6 +138,9 @@ export const ghidraMipsProfileParameters = (
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
},
|
||||
...(isGhidraMipsSoftFloatTarget(target)
|
||||
? { mips_soft_float_storage: GHIDRA_MIPS_SOFT_FLOAT_STORAGE }
|
||||
: {}),
|
||||
mips_support_lane: "elf32-exec-o32-arch32r2-standard-v3",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,6 +56,12 @@ import {
|
||||
GHIDRA_PSP_LIMITATIONS,
|
||||
} from "./GhidraPspProfile.js";
|
||||
import { ghidraPspExtensionSchema } from "./GhidraPspExtension.js";
|
||||
import {
|
||||
GHIDRA_MIPS_SOFT_FLOAT_STORAGE,
|
||||
ghidraMipsSoftFloatFailure,
|
||||
ghidraMipsSoftFloatLimitations,
|
||||
isGhidraMipsSoftFloatTarget,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js";
|
||||
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
|
||||
import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js";
|
||||
@@ -386,6 +392,20 @@ export const createGhidraProviderClient = (input: {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const softFloat = isGhidraMipsSoftFloatTarget(target);
|
||||
if (
|
||||
softFloat !==
|
||||
(committedProfile.parameters.mips_soft_float_storage ===
|
||||
GHIDRA_MIPS_SOFT_FLOAT_STORAGE)
|
||||
)
|
||||
return unavailableClient(
|
||||
new ProviderAdapterError("ghidra", "open_binary", {
|
||||
diagnostics: {
|
||||
reason:
|
||||
"Resolve the soft-float MIPS storage policy into the analysis profile before opening the session.",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const extensionProfile = ghidraExtensionSchema
|
||||
.array()
|
||||
.safeParse(committedProfile.parameters.analysis_extensions ?? []);
|
||||
@@ -465,6 +485,10 @@ export const createGhidraProviderClient = (input: {
|
||||
let extensionFailure: AnalysisError | undefined;
|
||||
// Replaced by the reported outcome once the session handshake is checked.
|
||||
const seedLimitations: string[] = [...ghidraSeedLimitations(seeds)];
|
||||
// Replaced by the reported storage outcome once the handshake is checked.
|
||||
const softFloatLimitations: string[] = [
|
||||
...ghidraMipsSoftFloatLimitations(softFloat),
|
||||
];
|
||||
const rawLanguage =
|
||||
target.format === "raw-binary"
|
||||
? ghidraRawBinaryLanguage(target.architecture)
|
||||
@@ -508,6 +532,7 @@ export const createGhidraProviderClient = (input: {
|
||||
...(pspExtension?.success === true
|
||||
? { pspExtension: pspExtension.data }
|
||||
: {}),
|
||||
...(softFloat ? { mipsSoftFloat: true as const } : {}),
|
||||
...(target.format === "dos-mz" ? { dosMz: true } : {}),
|
||||
...(target.format === "dos-com" ? { dosCom: true } : {}),
|
||||
...(rawLanguage === undefined || target.format !== "raw-binary"
|
||||
@@ -569,18 +594,37 @@ export const createGhidraProviderClient = (input: {
|
||||
operation: AnalysisOperation,
|
||||
info: GhidraSessionInfo,
|
||||
): Promise<AnalysisError | undefined> => {
|
||||
const softFloatReason = ghidraMipsSoftFloatFailure(
|
||||
softFloat,
|
||||
info.mips_soft_float_storage,
|
||||
);
|
||||
extensionFailure =
|
||||
ghidraExtensionFailure(
|
||||
extensions,
|
||||
info.analysis_extensions ?? [],
|
||||
operation,
|
||||
) ?? ghidraSeedFailure(seeds, info.analysis_seeds, operation);
|
||||
) ??
|
||||
ghidraSeedFailure(seeds, info.analysis_seeds, operation) ??
|
||||
(softFloatReason === undefined
|
||||
? undefined
|
||||
: new ProviderAdapterError("ghidra", operation, {
|
||||
diagnostics: { reason: softFloatReason },
|
||||
}));
|
||||
if (extensionFailure === undefined && info.analysis_seeds !== undefined)
|
||||
seedLimitations.splice(
|
||||
0,
|
||||
seedLimitations.length,
|
||||
...ghidraSeedLimitations(seeds, info.analysis_seeds),
|
||||
);
|
||||
if (extensionFailure === undefined)
|
||||
softFloatLimitations.splice(
|
||||
0,
|
||||
softFloatLimitations.length,
|
||||
...ghidraMipsSoftFloatLimitations(
|
||||
softFloat,
|
||||
info.mips_soft_float_storage,
|
||||
),
|
||||
);
|
||||
if (extensionFailure === undefined) return undefined;
|
||||
const closed = await client.close();
|
||||
return closed.ok
|
||||
@@ -598,6 +642,7 @@ export const createGhidraProviderClient = (input: {
|
||||
...providerLimitations,
|
||||
...targetLimitations,
|
||||
...(psp ? GHIDRA_PSP_LIMITATIONS : []),
|
||||
...softFloatLimitations,
|
||||
...ghidraExtensionLimitations(extensions),
|
||||
...seedLimitations,
|
||||
...(releaseLimitation === undefined ? [] : [releaseLimitation]),
|
||||
@@ -648,7 +693,7 @@ export const createGhidraProviderClient = (input: {
|
||||
? parseGhidraFunctionInput(operation, parameters)
|
||||
: parseGhidraInventoryInput(operation, parameters);
|
||||
if (!input.ok) return input;
|
||||
if (extensions.length > 0 || seeds !== undefined) {
|
||||
if (extensions.length > 0 || seeds !== undefined || softFloat) {
|
||||
const started = await client.start(options?.signal, options?.progress);
|
||||
if (!started.ok)
|
||||
return err(
|
||||
|
||||
@@ -52,6 +52,40 @@ describe("Ghidra DOS import commitment", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra MIPS soft-float storage report", () => {
|
||||
const valid = {
|
||||
policy: "o32-soft-float-prototype-v1",
|
||||
functions: 2,
|
||||
definitions: 1,
|
||||
};
|
||||
it("accepts the exact committed policy outcome", () => {
|
||||
const parsed = parseGhidraSessionInfo(
|
||||
{ ...session(), mips_soft_float_storage: valid },
|
||||
expected,
|
||||
);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(parsed.value.mips_soft_float_storage).toEqual(valid);
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
"the superseded storage policy",
|
||||
{ policy: "o32-integer-registers-v1", remapped: 2, retyped: 1 },
|
||||
],
|
||||
["a negative count", { ...valid, definitions: -1 }],
|
||||
["a fractional count", { ...valid, functions: 1.5 }],
|
||||
["a missing count", { policy: valid.policy, functions: 2 }],
|
||||
["an undeclared field", { ...valid, skipped: 0 }],
|
||||
["a non-object report", "functions"],
|
||||
])("rejects %s", (_label, report) => {
|
||||
expect(
|
||||
parseGhidraSessionInfo(
|
||||
{ ...session(), mips_soft_float_storage: report },
|
||||
expected,
|
||||
).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra mutation handshake", () => {
|
||||
it("requires the exact mutation authority and rejects duplicate capabilities", () => {
|
||||
const value = session();
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "./GhidraInventoryValues.js";
|
||||
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
|
||||
import { ghidraExtensionResultSchema } from "./extensions/GhidraExtensions.js";
|
||||
import { GHIDRA_MIPS_SOFT_FLOAT_STORAGE } from "./GhidraMipsProfile.js";
|
||||
|
||||
/** Exact methods proved by the bridge handshake after auto-analysis. */
|
||||
export const GHIDRA_SESSION_CAPABILITIES = [
|
||||
@@ -42,6 +43,14 @@ const sessionInfoSchema = z
|
||||
analysis_timed_out: z.boolean(),
|
||||
analysis_extensions: z.array(ghidraExtensionResultSchema).optional(),
|
||||
analysis_seeds: ghidraSeedReportSchema.optional(),
|
||||
mips_soft_float_storage: z
|
||||
.object({
|
||||
policy: z.literal(GHIDRA_MIPS_SOFT_FLOAT_STORAGE),
|
||||
functions: z.number().int().nonnegative(),
|
||||
definitions: z.number().int().nonnegative(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
capabilities: z.array(capabilitySchema),
|
||||
target: z
|
||||
.object({
|
||||
|
||||
@@ -191,6 +191,45 @@ describe("Ghidra headless launcher", () => {
|
||||
expect(arguments_).toContain("-readOnly");
|
||||
expect(arguments_).toContain("-deleteProject");
|
||||
});
|
||||
it("remaps soft-float storage after the no-return fix and before the bridge", () => {
|
||||
const base = {
|
||||
platform: "linux" as const,
|
||||
projectRoot: "/tmp/project",
|
||||
targetPath: "/tmp/target",
|
||||
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
||||
descriptorPath: "/tmp/session.json",
|
||||
ghidraLogPath: "/tmp/ghidra.log",
|
||||
scriptLogPath: "/tmp/script.log",
|
||||
};
|
||||
const soft = "/package/bridge/ReaGhidraMipsSoftFloat.java";
|
||||
expect(ghidraHeadlessArguments(base)).not.toContain(soft);
|
||||
const arguments_ = ghidraHeadlessArguments({
|
||||
...base,
|
||||
mipsSoftFloat: true,
|
||||
});
|
||||
expect(arguments_[arguments_.indexOf(soft) - 1]).toBe("-postScript");
|
||||
expect(
|
||||
arguments_.indexOf("/package/bridge/ReaGhidraNoReturnFix.java"),
|
||||
).toBeLessThan(arguments_.indexOf(soft));
|
||||
expect(arguments_.indexOf(soft)).toBeLessThan(
|
||||
arguments_.indexOf("/package/bridge/ReaGhidraBridge.java"),
|
||||
);
|
||||
for (const conflict of [
|
||||
{ psp: true as const },
|
||||
{ dosMz: true as const },
|
||||
{ dosCom: true as const },
|
||||
{
|
||||
rawBinary: {
|
||||
languageId: "x86:LE:32:default",
|
||||
compilerSpecId: "default",
|
||||
baseAddr: "0x1000",
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(() =>
|
||||
ghidraHeadlessArguments({ ...base, mipsSoftFloat: true, ...conflict }),
|
||||
).toThrow(/generic MIPS ELF/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra configured import language and seeds", () => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* Soft-float o32 storage fixture; linked with mips.c, never executed by REA.
|
||||
* Without an FPU, float/double values travel in integer registers and stack
|
||||
* slots: pick uses $a0/$a1/$a2 and returns in $v0, wide uses the $a0:$a1 pair,
|
||||
* and mixed aligns d to $a2:$a3 so f and e spill to the stack at 16 and 24.
|
||||
* structured returns through a hidden $a0 pointer, moving scale to $a1. The
|
||||
* typed function pointer makes the caller's indirect call use a prototype that
|
||||
* belongs to no function. No arithmetic, so no compiler-rt helpers are needed.
|
||||
*/
|
||||
volatile unsigned int rea_mips_soft_sink;
|
||||
|
||||
__attribute__((noinline)) float rea_mips_soft_pick(float a, float b, int sel)
|
||||
{
|
||||
return sel ? a : b;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) double rea_mips_soft_wide(double a, int sel)
|
||||
{
|
||||
return sel ? a : 0.5;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) double rea_mips_soft_mixed(int i, double d, float f, double e)
|
||||
{
|
||||
rea_mips_soft_sink = *(volatile unsigned int *)&f;
|
||||
return i ? d : e;
|
||||
}
|
||||
|
||||
struct rea_mips_soft_pair {
|
||||
double first;
|
||||
double second;
|
||||
};
|
||||
|
||||
__attribute__((noinline)) struct rea_mips_soft_pair rea_mips_soft_struct(float scale, int sel)
|
||||
{
|
||||
struct rea_mips_soft_pair pair = { 0.25, 0.75 };
|
||||
rea_mips_soft_sink = *(volatile unsigned int *)&scale;
|
||||
if (sel)
|
||||
pair.first = 1.25;
|
||||
return pair;
|
||||
}
|
||||
|
||||
/* Volatile so the indirect call through this typed pointer is not folded. */
|
||||
float (*volatile rea_mips_soft_callback)(float, float, int) = rea_mips_soft_pick;
|
||||
|
||||
__attribute__((noinline)) unsigned int rea_mips_soft_caller(int sel)
|
||||
{
|
||||
float picked = rea_mips_soft_pick(1.5f, 2.5f, sel);
|
||||
double wide = rea_mips_soft_wide(3.25, sel);
|
||||
double mixed = rea_mips_soft_mixed(sel, 4.75, 5.5f, 6.125);
|
||||
struct rea_mips_soft_pair pair = rea_mips_soft_struct(7.5f, sel);
|
||||
float called = rea_mips_soft_callback(8.5f, 9.5f, sel);
|
||||
return *(unsigned int *)&picked ^ *(unsigned int *)&wide ^ *(unsigned int *)&mixed ^
|
||||
*(unsigned int *)&pair.first ^ *(unsigned int *)&called;
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
|
||||
|
||||
export type MipsFloatAbi = "hard" | "soft";
|
||||
export interface MipsReadelfFacts {
|
||||
mips: MipsElfMetadata;
|
||||
symbols: Record<
|
||||
| "rea_mips_entry"
|
||||
| "rea_mips_leaf"
|
||||
| "rea_mips_probe"
|
||||
| "rea_mips_global"
|
||||
| "rea_mips_marker",
|
||||
string
|
||||
>;
|
||||
symbols: Record<string, string>;
|
||||
}
|
||||
export function parseMipsReadelf(
|
||||
text: string,
|
||||
byteOrder: string,
|
||||
floatAbi?: MipsFloatAbi,
|
||||
): MipsReadelfFacts;
|
||||
export function inspectMipsReadelf(
|
||||
path: string,
|
||||
byteOrder: string,
|
||||
floatAbi?: MipsFloatAbi,
|
||||
): Promise<
|
||||
MipsReadelfFacts & { command: string; version: string; raw: string }
|
||||
>;
|
||||
@@ -33,3 +29,7 @@ export function assertMipsProbe(
|
||||
address: string,
|
||||
): void;
|
||||
export function assertMipsGlobal(bytes: string, byteOrder: string): void;
|
||||
export function assertMipsSoftFloatStorage(
|
||||
boundaries: Record<string, unknown>,
|
||||
byteOrder: string,
|
||||
): void;
|
||||
|
||||
@@ -15,8 +15,17 @@ const wordHex = (value, byteOrder) => {
|
||||
return bytes.toString("hex");
|
||||
};
|
||||
|
||||
const SOFT_FLOAT_SYMBOLS = [
|
||||
"rea_mips_soft_pick",
|
||||
"rea_mips_soft_wide",
|
||||
"rea_mips_soft_mixed",
|
||||
"rea_mips_soft_struct",
|
||||
"rea_mips_soft_caller",
|
||||
];
|
||||
|
||||
/** Parse only the documented GNU readelf fields needed by our tiny fixture. */
|
||||
export function parseMipsReadelf(text, byteOrder) {
|
||||
export function parseMipsReadelf(text, byteOrder, floatAbi = "hard") {
|
||||
assert.ok(floatAbi === "hard" || floatAbi === "soft");
|
||||
language(byteOrder);
|
||||
const field = (expression, label) => {
|
||||
const matches = [...text.matchAll(expression)];
|
||||
@@ -44,8 +53,14 @@ export function parseMipsReadelf(text, byteOrder) {
|
||||
const fpAbi = new Map([
|
||||
["Hard float (double precision)", 1],
|
||||
["Hard float (32-bit CPU, Any FPU)", 5],
|
||||
["Soft float", 3],
|
||||
]).get(fp);
|
||||
assert.ok(fpAbi !== undefined, `Unrecognized fixture FP ABI: ${fp}`);
|
||||
assert.equal(
|
||||
fpAbi === 3,
|
||||
floatAbi === "soft",
|
||||
`Fixture FP ABI ${fp} differs from the ${floatAbi}-float build intent`,
|
||||
);
|
||||
assert.equal(field(/^ISA Extension:\s*(.+)$/gmu, "ISA extension"), "None");
|
||||
assert.equal(field(/^ASEs:\s*\n[ \t]*(.+)$/gmu, "ASEs"), "None");
|
||||
const abiFlags = {
|
||||
@@ -76,6 +91,7 @@ export function parseMipsReadelf(text, byteOrder) {
|
||||
"rea_mips_probe",
|
||||
"rea_mips_global",
|
||||
"rea_mips_marker",
|
||||
...(floatAbi === "soft" ? SOFT_FLOAT_SYMBOLS : []),
|
||||
]) {
|
||||
const value = field(
|
||||
new RegExp(
|
||||
@@ -93,7 +109,7 @@ export function parseMipsReadelf(text, byteOrder) {
|
||||
}
|
||||
|
||||
/** Independent reader is a bounded test prerequisite, never an REA provider. */
|
||||
export async function inspectMipsReadelf(path, byteOrder) {
|
||||
export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
|
||||
const command = process.env.REA_MIPS_READELF ?? "readelf";
|
||||
const options = {
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
@@ -117,7 +133,7 @@ export async function inspectMipsReadelf(path, byteOrder) {
|
||||
`readelf diagnostics: ${result.stderr}`,
|
||||
);
|
||||
return {
|
||||
...parseMipsReadelf(result.stdout, byteOrder),
|
||||
...parseMipsReadelf(result.stdout, byteOrder, floatAbi),
|
||||
command,
|
||||
version: version.stdout.split("\n")[0],
|
||||
raw: result.stdout,
|
||||
@@ -178,3 +194,67 @@ export function assertMipsProbe(move, branch, byteOrder, address) {
|
||||
export function assertMipsGlobal(bytes, byteOrder) {
|
||||
assert.equal(bytes, wordHex(7, byteOrder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-float o32 places float/double values exactly where a same-size integer
|
||||
* goes: Ghidra joins register pairs most-significant word first. The struct
|
||||
* return keeps its hidden $a0 pointer and its float in $a1.
|
||||
*/
|
||||
export function assertMipsSoftFloatStorage(boundaries, byteOrder) {
|
||||
language(byteOrder);
|
||||
const pair = (low, high) =>
|
||||
byteOrder === "little" ? `${high}:4,${low}:4` : `${low}:4,${high}:4`;
|
||||
const expected = {
|
||||
rea_mips_soft_pick: {
|
||||
return: ["float", "v0:4"],
|
||||
parameters: [
|
||||
["a", "float", "a0:4"],
|
||||
["b", "float", "a1:4"],
|
||||
["sel", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
rea_mips_soft_wide: {
|
||||
return: ["double", pair("v0", "v1")],
|
||||
parameters: [
|
||||
["a", "double", pair("a0", "a1")],
|
||||
["sel", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
rea_mips_soft_mixed: {
|
||||
return: ["double", pair("v0", "v1")],
|
||||
parameters: [
|
||||
["i", "int", "a0:4"],
|
||||
["d", "double", pair("a2", "a3")],
|
||||
["f", "float", "Stack[0x10]:4"],
|
||||
["e", "double", "Stack[0x18]:8"],
|
||||
],
|
||||
},
|
||||
rea_mips_soft_struct: {
|
||||
return: ["rea_mips_soft_pair *", "v0:4"],
|
||||
parameters: [
|
||||
["__return_storage_ptr__", "rea_mips_soft_pair *", "a0:4 (auto)"],
|
||||
["scale", "float", "a1:4"],
|
||||
["sel", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
};
|
||||
assert.deepEqual(
|
||||
Object.keys(boundaries).sort(),
|
||||
Object.keys(expected).sort(),
|
||||
);
|
||||
for (const [name, want] of Object.entries(expected)) {
|
||||
const boundary = boundaries[name];
|
||||
assert.deepEqual(
|
||||
[boundary.return_type.data_type, boundary.return_type.storage],
|
||||
want.return,
|
||||
`${name} return storage`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...boundary.parameters]
|
||||
.sort((left, right) => left.ordinal - right.ordinal)
|
||||
.map((item) => [item.name, item.data_type, item.storage]),
|
||||
want.parameters,
|
||||
`${name} parameter storage`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
assertMipsGlobal,
|
||||
assertMipsLoadedImage,
|
||||
assertMipsProbe,
|
||||
assertMipsSoftFloatStorage,
|
||||
parseMipsReadelf,
|
||||
} from "./mips-checks.mjs";
|
||||
|
||||
@@ -227,3 +228,117 @@ describe("MIPS real-verifier independent expectations", () => {
|
||||
expect(() => assertMipsGlobal("0700", "little")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIPS soft-float real-verifier expectations", () => {
|
||||
const softReadelf = readelf
|
||||
.replace("CPR1 size: 32", "CPR1 size: 0")
|
||||
.replace("Hard float (32-bit CPU, Any FPU)", "Soft float")
|
||||
.replace(
|
||||
" 8: 00020240 32 FUNC GLOBAL DEFAULT 4 rea_mips_probe\n",
|
||||
[
|
||||
" 8: 00020240 32 FUNC GLOBAL DEFAULT 4 rea_mips_probe",
|
||||
" 9: 00020300 64 FUNC GLOBAL DEFAULT 4 rea_mips_soft_pick",
|
||||
" 10: 00020340 64 FUNC GLOBAL DEFAULT 4 rea_mips_soft_wide",
|
||||
" 11: 00020380 96 FUNC GLOBAL DEFAULT 4 rea_mips_soft_mixed",
|
||||
" 12: 000203e0 96 FUNC GLOBAL DEFAULT 4 rea_mips_soft_struct",
|
||||
" 13: 00020440 160 FUNC GLOBAL DEFAULT 4 rea_mips_soft_caller",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
it("reads the soft-float ABI record and its fixture symbols", () => {
|
||||
const parsed = parseMipsReadelf(softReadelf, "little", "soft");
|
||||
expect(parsed.mips.abiFlags).toMatchObject({ cpr1Size: 0, fpAbi: 3 });
|
||||
expect(parsed.symbols.rea_mips_soft_mixed).toBe("0x20380");
|
||||
});
|
||||
|
||||
it("rejects a float ABI that differs from the build intent", () => {
|
||||
expect(() => parseMipsReadelf(softReadelf, "little", "hard")).toThrow(
|
||||
/build intent/u,
|
||||
);
|
||||
expect(() => parseMipsReadelf(readelf, "little", "soft")).toThrow(
|
||||
/build intent/u,
|
||||
);
|
||||
});
|
||||
|
||||
const boundaries = (byteOrder: "little" | "big") => {
|
||||
const pair = (low: string, high: string) =>
|
||||
byteOrder === "little" ? `${high}:4,${low}:4` : `${low}:4,${high}:4`;
|
||||
const item = (
|
||||
ordinal: number,
|
||||
name: string,
|
||||
dataType: string,
|
||||
storage: string,
|
||||
) => ({ ordinal, name, data_type: dataType, storage });
|
||||
return {
|
||||
rea_mips_soft_pick: {
|
||||
return_type: { data_type: "float", storage: "v0:4" },
|
||||
parameters: [
|
||||
item(2, "sel", "int", "a2:4"),
|
||||
item(0, "a", "float", "a0:4"),
|
||||
item(1, "b", "float", "a1:4"),
|
||||
],
|
||||
},
|
||||
rea_mips_soft_wide: {
|
||||
return_type: { data_type: "double", storage: pair("v0", "v1") },
|
||||
parameters: [
|
||||
item(0, "a", "double", pair("a0", "a1")),
|
||||
item(1, "sel", "int", "a2:4"),
|
||||
],
|
||||
},
|
||||
rea_mips_soft_mixed: {
|
||||
return_type: { data_type: "double", storage: pair("v0", "v1") },
|
||||
parameters: [
|
||||
item(0, "i", "int", "a0:4"),
|
||||
item(1, "d", "double", pair("a2", "a3")),
|
||||
item(2, "f", "float", "Stack[0x10]:4"),
|
||||
item(3, "e", "double", "Stack[0x18]:8"),
|
||||
],
|
||||
},
|
||||
rea_mips_soft_struct: {
|
||||
return_type: { data_type: "rea_mips_soft_pair *", storage: "v0:4" },
|
||||
parameters: [
|
||||
item(
|
||||
0,
|
||||
"__return_storage_ptr__",
|
||||
"rea_mips_soft_pair *",
|
||||
"a0:4 (auto)",
|
||||
),
|
||||
item(1, "scale", "float", "a1:4"),
|
||||
item(2, "sel", "int", "a2:4"),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it("checks soft-float o32 storage, including pair order, alignment and spill", () => {
|
||||
assertMipsSoftFloatStorage(boundaries("little"), "little");
|
||||
assertMipsSoftFloatStorage(boundaries("big"), "big");
|
||||
expect(() =>
|
||||
assertMipsSoftFloatStorage(boundaries("big"), "little"),
|
||||
).toThrow();
|
||||
const hardFloat = boundaries("little");
|
||||
const pickA = hardFloat.rea_mips_soft_pick.parameters.find(
|
||||
(item) => item.ordinal === 0,
|
||||
);
|
||||
if (pickA === undefined) throw new Error("fixture lacks pick parameter a");
|
||||
pickA.storage = "f12:4";
|
||||
expect(() => assertMipsSoftFloatStorage(hardFloat, "little")).toThrow();
|
||||
const unaligned = boundaries("little");
|
||||
const mixedD = unaligned.rea_mips_soft_mixed.parameters.find(
|
||||
(item) => item.ordinal === 1,
|
||||
);
|
||||
if (mixedD === undefined)
|
||||
throw new Error("fixture lacks mixed parameter d");
|
||||
mixedD.storage = "a2:4,a1:4";
|
||||
expect(() => assertMipsSoftFloatStorage(unaligned, "little")).toThrow();
|
||||
const lostHiddenReturn = boundaries("little");
|
||||
lostHiddenReturn.rea_mips_soft_struct.parameters = [
|
||||
{ ordinal: 0, name: "scale", data_type: "float", storage: "a0:4" },
|
||||
{ ordinal: 1, name: "sel", data_type: "int", storage: "a1:4" },
|
||||
];
|
||||
expect(() =>
|
||||
assertMipsSoftFloatStorage(lostHiddenReturn, "little"),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,18 +8,29 @@ 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) {
|
||||
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}.elf`);
|
||||
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",
|
||||
@@ -30,6 +41,7 @@ export async function buildMipsFixture(directory, byteOrder) {
|
||||
"-Wl,-e,rea_mips_entry",
|
||||
"-Wl,--build-id=none",
|
||||
source,
|
||||
...(soft ? [softSource] : []),
|
||||
probe,
|
||||
"-o",
|
||||
path,
|
||||
@@ -60,11 +72,19 @@ export async function buildMipsFixture(directory, byteOrder) {
|
||||
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"),
|
||||
|
||||
Reference in New Issue
Block a user