feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention (#8229)
* feat(ghidra): record the RISC-V float-ABI calling-convention decision Decision D3 (#7477), provisional: admit ELF32 soft-float (ilp32) RISC-V images through a lane that keeps the pinned RISCV:LE:32:default language and corrects the calling convention in the bridge. This commit is the contract only; admission is unchanged. - The domain owns the e_flags float-ABI vocabulary and parses .riscv.attributes and Tag_RISCV_arch into a declared, absent or unavailable ISA declaration. - The Ghidra profile owns the lane table (class and float ABI), lane ids built from fields, the ilp32-prototype-v1 policy and its __rea_ilp32 prototype, and the extensions Ghidra 12.1.4 decodes with p-code. - The session handshake accepts the bridge's calling-convention report. - docs/ghidra-riscv.md keeps the decision record and per-ABI policies for ilp32 and ilp32f; a test keeps it in step with the tables. - tests/conformance/c/riscv-float-abi.c is the source the policies were measured on. * feat(ghidra): admit RISC-V ilp32 ELF with a derived calling convention ESP32-C2, C3, C5, C6, C61, H2 and H21 builds are ELF32 soft-float (ilp32) images, which the RISC-V lane refused at its double-float check. Ghidra 12.1.4's generic RV32 compiler specification models ilp32d, so its stock prototype would read float and double arguments from fa0-fa7. - Target resolution reads the one SHT_RISCV_ATTRIBUTES section through the same bounded ELF declaration reader as range validation, and records Tag_RISCV_arch as declared, absent or unavailable. - Admission selects a lane from the class and float-ABI table. The soft-float lane also requires an RV32I Tag_RISCV_arch that declares only extensions Ghidra decodes with p-code; the refusal names any others (Zcb, Zcmp, Zcmt, Zb*, xesp*). - Soft-float profiles commit float_abi, arch, riscv_calling_convention ilp32-prototype-v1 and the lane elf32-exec-le-soft-float-generic-v1. Double-float profiles are unchanged. - ReaGhidraRiscvCallingConvention.java runs after analysis. It derives __rea_ilp32 from the default prototype without floating-point entries and rules, with fs0-fs11 killed by calls, self-checks its storage and assigns it to every non-thunk function, external function and function definition. Its report is written last. - The provider client requires that report exactly when the profile commits the policy, before any operation, and keeps it as session limitations and caller-project history. ELF32 single-float (ilp32f), quad-float and ELF64 soft- or single-float images stay refused, now with a reason built from the lane table. * test(ghidra): verify RISC-V ilp32 against real Ghidra verify:ghidra:riscv now also builds tests/conformance/c/riscv-float-abi.c with ESP32-C3's -march=rv32imc_zicsr_zifencei -mabi=ilp32, typed with DWARF and linked again with --strip-all. Every allocated section of the stripped image must keep its address and bytes. - riscv-checks.mjs holds the ilp32 storage of each fixture function together with the words the compiler's call site writes there. An LLVM objdump reading of the caller must find every word at its stated location: an unaligned named double, the a7/stack split, two floats in an integer pair, a large structure by reference, aligned variadic pairs and an indirect call. - Through CLI and stdio MCP, the soft-float builds must report the soft-float lane and ilp32-prototype-v1, __rea_ilp32 with exactly that storage (one integer word per location when stripped), caller pseudocode that passes the source constants and names no floating-point register, the prototype outcome in retained Evidence, and decoded fence.i and csrr. - The RV32 double-float image relabelled as soft-float, refused before, is admitted through the new lane. RV64 soft-float, single-float and unsupported bits are refused with their reasons. - riscv-checks.test.ts runs the checks offline against recorded LLVM and Ghidra output, including Ghidra's stock ilp32d prototypes as a negative control. * fix(ghidra): pass RISC-V ilp32 values over 8 bytes by reference The psABI passes any value wider than 2 x XLEN by reference, scalars included. __rea_ilp32 converted only structures and unions over 8 bytes to pointers, so a long double (16 bytes) was joined into a1-a4. Measured on Ghidra 12.1.4, rea_riscv_abi_quad(long double value, int sel) read value from a4:4,a3:4,a2:4,a1:4 and sel from a5, where clang passes a pointer in a1 and sel in a2. - ReaGhidraRiscvCallingConvention.java now has one derivation driven by a per-policy row (floating-point entry size, preserved floating-point registers). Every policy inserts a rule before the input list's final join that converts any value of 9 bytes or more to a pointer; a return that size already becomes a caller buffer. The self-check requires a 16-byte value to pass by reference. - The fixture gains rea_riscv_abi_quad. The call-site oracle now checks the words the by-reference pointers point at, for the large structure and the long double, and the recorded data comes from an LLVM 19 build. - docs/ghidra-riscv.md: the decision record and the ilp32 policy row. ilp32-prototype-v1 keeps its id: no release carries it yet. * test(ghidra): check RISC-V lane limitations by their facts The limitation tests froze a full explanatory sentence, the note count and array positions, so a harmless rewording failed them: the pattern Greptile flagged on #7486. Assert the facts the notes must carry instead: the pinned language, the static-linkage boundary, one note naming the declared ABI, the ABI the pinned language models and the prototype REA applies, and the declared ISA. * test(ghidra): check return storage in stripped RISC-V ilp32 builds The stripped branch of assertRiscvIlp32Storage compared parameter storage only, so an inferred return that lost the high word of a register pair, such as rea_riscv_abi_wide at a0:4 instead of a1:4,a0:4, still passed. Stripped returns must now use the typed build's storage, or none when the result goes through caller memory. Recovered type names are still not compared: a test accepts other type names and refuses the dropped high word. * docs(ghidra): record decision D3 as decided in #7477
This commit is contained in:
1 parent
e6d0618979
commit
e669365c3b
29 files changed
+3675
-264
No files matched your search
@@ -548,6 +548,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
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));
|
||||
// Written by ReaGhidraRiscvCallingConvention after analysis; REA requires it
|
||||
// exactly when the committed profile declares a RISC-V calling-convention policy.
|
||||
String riscvCallingConvention = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getString("REA RISC-V calling convention", null);
|
||||
if (riscvCallingConvention != null) result.add("riscv_calling_convention", JsonParser.parseString(riscvCallingConvention));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.database.SpecExtension;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.data.DoubleDataType;
|
||||
import ghidra.program.model.data.Float16DataType;
|
||||
import ghidra.program.model.data.FloatDataType;
|
||||
import ghidra.program.model.data.FunctionDefinition;
|
||||
import ghidra.program.model.data.IntegerDataType;
|
||||
import ghidra.program.model.data.StructureDataType;
|
||||
import ghidra.program.model.lang.CompilerSpec;
|
||||
import ghidra.program.model.lang.Language;
|
||||
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.Varnode;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Apply the committed RISC-V calling-convention policy after analysis. Ghidra's generic RV32
|
||||
* compiler specification models ilp32d. Each policy derives a program-specific prototype from the
|
||||
* program's own default prototype, as its {@link Policy} row and docs/ghidra-riscv.md state. The
|
||||
* script installs the prototype as a specification extension and assigns it, with dynamic
|
||||
* storage, to every non-thunk function, external function and function definition.
|
||||
*/
|
||||
public final class ReaGhidraRiscvCallingConvention extends GhidraScript {
|
||||
static final String REPORT_KEY = "REA RISC-V calling convention";
|
||||
/** RISC-V floating-point registers by ABI name: ft0-ft11, fs0-fs11 and fa0-fa7. */
|
||||
private static final Pattern FLOAT_REGISTER = Pattern.compile("f[ast]\\d+");
|
||||
|
||||
/**
|
||||
* What a policy changes in the default ilp32d prototype. Every policy also passes values of 9
|
||||
* bytes or more, such as {@code long double}, by reference.
|
||||
*
|
||||
* @param floatEntryBytes the largest value a floating-point register may pass or return; 0
|
||||
* removes those entries and every rule that assigns floating-point storage
|
||||
* @param preservesFloatRegisters whether fs0-fs11 stay preserved across calls
|
||||
*/
|
||||
private record Policy(
|
||||
String id, String prototype, int floatEntryBytes, boolean preservesFloatRegisters) {}
|
||||
|
||||
private static final List<Policy> POLICIES =
|
||||
List.of(new Policy("ilp32-prototype-v1", "__rea_ilp32", 0, false));
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
Policy policy =
|
||||
POLICIES.stream()
|
||||
.filter(candidate -> arguments.length == 1 && candidate.id().equals(arguments[0]))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
"REA RISC-V calling convention requires one of the policies "
|
||||
+ POLICIES.stream().map(Policy::id).toList()));
|
||||
Language language = currentProgram.getLanguage();
|
||||
if (!language.getProcessor().toString().equals("RISCV")
|
||||
|| currentProgram.getDefaultPointerSize() != 4)
|
||||
throw new IllegalStateException(
|
||||
"REA " + policy.id() + " requires a 32-bit RISC-V program, not "
|
||||
+ language.getLanguageID());
|
||||
CompilerSpec spec = currentProgram.getCompilerSpec();
|
||||
new SpecExtension(currentProgram)
|
||||
.addReplaceCompilerSpecExtension(
|
||||
derive(spec.getDefaultCallingConvention(), spec, language, policy), monitor);
|
||||
PrototypeModel model =
|
||||
currentProgram.getCompilerSpec().getCallingConvention(policy.prototype());
|
||||
if (model == null)
|
||||
throw new IllegalStateException("REA " + policy.prototype() + " was not installed");
|
||||
selfCheck(model, policy, language);
|
||||
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()) continue;
|
||||
if (function.hasCustomVariableStorage()) function.setCustomVariableStorage(false);
|
||||
function.setCallingConvention(policy.prototype());
|
||||
if (policy.floatEntryBytes() == 0) {
|
||||
List<VariableStorage> storage = new ArrayList<>();
|
||||
storage.add(function.getReturn().getVariableStorage());
|
||||
for (Parameter parameter : function.getParameters())
|
||||
storage.add(parameter.getVariableStorage());
|
||||
// An untyped function has no assigned storage yet; the decompiler applies the
|
||||
// prototype when it recovers one.
|
||||
requireIntegerStorage(
|
||||
storage.toArray(VariableStorage[]::new), function.getName(true), true);
|
||||
}
|
||||
functions += 1;
|
||||
}
|
||||
int definitions = 0;
|
||||
Iterator<DataType> types = currentProgram.getDataTypeManager().getAllDataTypes();
|
||||
while (types.hasNext()) {
|
||||
if (!(types.next() instanceof FunctionDefinition definition)) continue;
|
||||
definition.setCallingConvention(policy.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.id() + "\",\"functions\":" + functions
|
||||
+ ",\"definitions\":" + definitions + "}");
|
||||
println(
|
||||
"REA RISC-V " + policy.prototype() + ": functions=" + functions
|
||||
+ " definitions=" + definitions);
|
||||
}
|
||||
|
||||
/** The default ilp32d prototype changed as the policy states. */
|
||||
private static String derive(
|
||||
PrototypeModel base, CompilerSpec spec, Language language, Policy policy) 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", policy.prototype());
|
||||
List<Element> floatEntries = new ArrayList<>();
|
||||
NodeList entries = root.getElementsByTagName("pentry");
|
||||
for (int index = 0; index < entries.getLength(); index += 1) {
|
||||
Element entry = (Element) entries.item(index);
|
||||
if (entry.getAttribute("storage").equals("float")) floatEntries.add(entry);
|
||||
}
|
||||
if (floatEntries.isEmpty())
|
||||
throw new IllegalStateException("Default prototype has no floating-point entries");
|
||||
if (policy.floatEntryBytes() == 0) {
|
||||
List<Node> removed = new ArrayList<>(floatEntries);
|
||||
NodeList rules = root.getElementsByTagName("rule");
|
||||
for (int index = 0; index < rules.getLength(); index += 1) {
|
||||
Element rule = (Element) rules.item(index);
|
||||
if (usesFloatStorage(rule)) removed.add(rule);
|
||||
}
|
||||
if (removed.size() == floatEntries.size())
|
||||
throw new IllegalStateException("Default prototype has no floating-point rules to remove");
|
||||
for (Node node : removed) node.getParentNode().removeChild(node);
|
||||
} else
|
||||
for (Element entry : floatEntries)
|
||||
entry.setAttribute("maxsize", Integer.toString(policy.floatEntryBytes()));
|
||||
// Values over 2 x XLEN go by reference. Returns need no rule: a value that the return
|
||||
// registers cannot hold already becomes a caller buffer passed in a0.
|
||||
NodeList inputs = root.getElementsByTagName("input");
|
||||
if (inputs.getLength() != 1)
|
||||
throw new IllegalStateException("Default prototype has no single input list");
|
||||
Element input = (Element) inputs.item(0);
|
||||
Element last = null;
|
||||
for (Node child = input.getFirstChild(); child != null; child = child.getNextSibling())
|
||||
if (child instanceof Element element && element.getTagName().equals("rule")) last = element;
|
||||
if (last == null || !joinsAnyValue(last))
|
||||
throw new IllegalStateException("Default prototype's input list does not end with a join");
|
||||
Element byReference = document.createElement("rule");
|
||||
Element datatype = document.createElement("datatype");
|
||||
datatype.setAttribute("name", "any");
|
||||
datatype.setAttribute("minsize", "9");
|
||||
byReference.appendChild(datatype);
|
||||
byReference.appendChild(document.createElement("convert_to_ptr"));
|
||||
input.insertBefore(byReference, last);
|
||||
if (!policy.preservesFloatRegisters()) {
|
||||
NodeList killedLists = root.getElementsByTagName("killedbycall");
|
||||
Element killed = killedLists.getLength() == 0 ? null : (Element) killedLists.item(0);
|
||||
if (killed == null) {
|
||||
killed = document.createElement("killedbycall");
|
||||
root.appendChild(killed);
|
||||
}
|
||||
List<Element> moved = new ArrayList<>();
|
||||
NodeList preservedLists = root.getElementsByTagName("unaffected");
|
||||
for (int list = 0; list < preservedLists.getLength(); list += 1) {
|
||||
NodeList preserved = preservedLists.item(list).getChildNodes();
|
||||
for (int index = 0; index < preserved.getLength(); index += 1) {
|
||||
if (preserved.item(index) instanceof Element varnode
|
||||
&& isFloatRegister(varnode, language))
|
||||
moved.add(varnode);
|
||||
}
|
||||
}
|
||||
for (Element varnode : moved) {
|
||||
varnode.getParentNode().removeChild(varnode);
|
||||
killed.appendChild(varnode);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/** The installed prototype places each class of value as its policy states. */
|
||||
private void selfCheck(PrototypeModel model, Policy policy, Language language) {
|
||||
StructureDataType pair = new StructureDataType("rea_float_pair", 0);
|
||||
pair.add(FloatDataType.dataType);
|
||||
pair.add(FloatDataType.dataType);
|
||||
VariableStorage[] storage =
|
||||
model.getStorageLocations(
|
||||
currentProgram,
|
||||
new DataType[] {
|
||||
DoubleDataType.dataType, FloatDataType.dataType, DoubleDataType.dataType,
|
||||
IntegerDataType.dataType, pair, Float16DataType.dataType
|
||||
},
|
||||
false);
|
||||
VariableStorage quad = storage[storage.length - 1];
|
||||
if (quad.size() != currentProgram.getDefaultPointerSize() || quad.getRegister() == null)
|
||||
throw new IllegalStateException(
|
||||
"REA " + policy.prototype() + " does not pass a 16-byte value by reference: " + quad);
|
||||
if (policy.floatEntryBytes() == 0)
|
||||
requireIntegerStorage(storage, policy.prototype() + " self-check", false);
|
||||
if (policy.preservesFloatRegisters()) return;
|
||||
for (Varnode preserved : model.getUnaffectedList()) {
|
||||
Register register = language.getRegister(preserved.getAddress(), preserved.getSize());
|
||||
if (register != null && FLOAT_REGISTER.matcher(register.getName()).matches())
|
||||
throw new IllegalStateException(
|
||||
"REA " + policy.prototype() + " still preserves " + register.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean joinsAnyValue(Element rule) {
|
||||
List<Element> children = new ArrayList<>();
|
||||
for (Node child = rule.getFirstChild(); child != null; child = child.getNextSibling())
|
||||
if (child instanceof Element element) children.add(element);
|
||||
return children.size() == 2
|
||||
&& children.get(0).getTagName().equals("datatype")
|
||||
&& children.get(0).getAttribute("name").equals("any")
|
||||
&& children.get(1).getTagName().equals("join");
|
||||
}
|
||||
|
||||
/** Whether a rule's action, at any depth, assigns floating-point storage. */
|
||||
private static boolean usesFloatStorage(Element element) {
|
||||
NodeList children = element.getChildNodes();
|
||||
for (int index = 0; index < children.getLength(); index += 1) {
|
||||
if (!(children.item(index) instanceof Element child)) continue;
|
||||
if (child.getAttribute("storage").equals("float") || usesFloatStorage(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isFloatRegister(Element varnode, Language language) {
|
||||
if (!varnode.getTagName().equals("varnode")
|
||||
|| !varnode.getAttribute("space").equals("register"))
|
||||
return false;
|
||||
Register register =
|
||||
language.getRegister(
|
||||
language.getAddressFactory().getRegisterSpace()
|
||||
.getAddress(Long.decode(varnode.getAttribute("offset"))),
|
||||
Integer.parseInt(varnode.getAttribute("size")));
|
||||
return register != null && FLOAT_REGISTER.matcher(register.getName()).matches();
|
||||
}
|
||||
|
||||
private static void requireIntegerStorage(
|
||||
VariableStorage[] storage, String owner, boolean allowUnassigned) {
|
||||
for (VariableStorage item : storage) {
|
||||
if (item.isBadStorage() || (!allowUnassigned && item.isUnassignedStorage()))
|
||||
throw new IllegalStateException("REA RISC-V storage unresolved for " + owner);
|
||||
List<Register> registers = item.getRegisters();
|
||||
if (registers == null) continue; // stack-only storage
|
||||
for (Register register : registers) {
|
||||
if (FLOAT_REGISTER.matcher(register.getName()).matches())
|
||||
throw new IllegalStateException(
|
||||
"REA RISC-V storage still uses " + register.getName() + " for " + owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+264
-40
@@ -9,29 +9,43 @@ lane shape as [generic MIPS support](ghidra-mips.md).
|
||||
|
||||
ELF `EM_RISCV` (243) is retained as the provider-neutral `riscv` family. The
|
||||
target also retains the raw ELF declarations that decide a lane: class, byte
|
||||
order, `e_type`, and `e_flags`. Recognition does not mean every provider or
|
||||
RISC-V variant is supported. Existing PE, managed and Mach-O architecture sets
|
||||
are unchanged. Headerless inputs use the separate [raw-binary admission
|
||||
order, `e_type`, `e_flags`, and the ISA string that `.riscv.attributes`
|
||||
declares. Recognition does not mean every provider or RISC-V variant is
|
||||
supported. Existing PE, managed and Mach-O architecture sets are unchanged.
|
||||
Headerless inputs use the separate [raw-binary admission
|
||||
boundary](raw-binary.md); raw RISC-V images are not supported.
|
||||
|
||||
The Ghidra lane admits little-endian ELF32 and ELF64 `ET_EXEC` images that
|
||||
declare `EF_RISCV_FLOAT_ABI_DOUBLE` in `e_flags`, with no `EF_RISCV_RVE`,
|
||||
`EF_RISCV_TSO`, or any bit outside `EF_RISCV_RVC | EF_RISCV_FLOAT_ABI`. These are
|
||||
header declarations, not proof that every instruction conforms to that ISA.
|
||||
The Ghidra lane admits little-endian `ET_EXEC` images with no `EF_RISCV_RVE`,
|
||||
`EF_RISCV_TSO`, or any bit outside `EF_RISCV_RVC | EF_RISCV_FLOAT_ABI`, in one
|
||||
of three lanes:
|
||||
|
||||
- ELF64 or ELF32 images that declare `EF_RISCV_FLOAT_ABI_DOUBLE` use Ghidra's
|
||||
stock double-float model unchanged. Their `.riscv.attributes` is not read for
|
||||
admission.
|
||||
- ELF32 images that declare the soft-float ABI (`e_flags` float-ABI bits `0`,
|
||||
ilp32) use REA's [ilp32 calling-convention policy](#ilp32). They must also
|
||||
declare an RV32I base and only [decoded extensions](#isa-declarations) in
|
||||
`Tag_RISCV_arch`.
|
||||
|
||||
These are header declarations, not proof that every instruction conforms to
|
||||
that ISA.
|
||||
|
||||
Provider selection returns the specific unsupported constraint rather than
|
||||
guessing a compatible profile:
|
||||
|
||||
| Declaration | Outcome |
|
||||
| ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Little-endian ELF64 `ET_EXEC`, double-float ABI | admitted, `RISCV:LE:64:default` + `gcc` |
|
||||
| Little-endian ELF32 `ET_EXEC`, double-float ABI | admitted, `RISCV:LE:32:default` + `gcc` |
|
||||
| Big-endian | refused; Ghidra 12.1.4 ships no big-endian RISC-V language |
|
||||
| `ET_REL`, `ET_DYN`, other `e_type` | refused with the declared value |
|
||||
| `EF_RISCV_RVE` | refused; the embedded 16-register profile (x0–x15) needs its own processor specification |
|
||||
| `EF_RISCV_TSO` | refused; the total-store-order memory model is unverified here |
|
||||
| Soft, single, or quad float ABI | refused; the pinned generic compiler specification models double-float |
|
||||
| Reserved or newer `e_flags` bits | refused with the offending bits named |
|
||||
| Declaration | Outcome |
|
||||
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| Little-endian ELF64 `ET_EXEC`, double-float ABI | admitted, `RISCV:LE:64:default` + `gcc`, lane `elf64-exec-le-double-float-generic-v1` |
|
||||
| Little-endian ELF32 `ET_EXEC`, double-float ABI | admitted, `RISCV:LE:32:default` + `gcc`, lane `elf32-exec-le-double-float-generic-v1` |
|
||||
| Little-endian ELF32 `ET_EXEC`, soft-float ABI, decoded extensions only | admitted, `RISCV:LE:32:default` + `gcc` with the `__rea_ilp32` prototype, lane `elf32-exec-le-soft-float-generic-v1` |
|
||||
| ELF32 soft-float ABI without `Tag_RISCV_arch`, or with any other extension | refused; the reason names the extensions outside the decoded set |
|
||||
| ELF32 single-float ABI (ilp32f) | refused until its [written policy](#ilp32f) is implemented and verified |
|
||||
| ELF64 soft- or single-float ABI (lp64, lp64f), or the quad-float ABI | refused; no verified calling-convention policy |
|
||||
| Big-endian | refused; Ghidra 12.1.4 ships no big-endian RISC-V language |
|
||||
| `ET_REL`, `ET_DYN`, other `e_type` | refused with the declared value |
|
||||
| `EF_RISCV_RVE` | refused; the embedded 16-register profile (x0–x15) needs its own processor specification |
|
||||
| `EF_RISCV_TSO` | refused; the total-store-order memory model is unverified here |
|
||||
| Reserved or newer `e_flags` bits | refused with the offending bits named |
|
||||
|
||||
`e_type` is the only load-shape declaration this lane reads. A fixed-address
|
||||
`ET_EXEC` declaration does **not** establish static linkage: `PT_INTERP` and
|
||||
@@ -58,15 +72,193 @@ with the `gcc` compiler specification per class, records
|
||||
pin without changing the requested override. An override that matches the pin is
|
||||
accepted and recorded as `configured-v1`.
|
||||
|
||||
The soft-float lane pins the same `RISCV:LE:32:default` language. Every RV32
|
||||
language in Ghidra 12.1.4, including the deprecated soft-float `RV32I`, `RV32IC`
|
||||
and `RV32IMC` variants in `old/riscv_deprecated.ldefs`, uses the same
|
||||
`riscv.ilp32d.sla` decoder; only the compiler specification differs. REA keeps
|
||||
the generic language and corrects the calling convention instead.
|
||||
|
||||
## Calling conventions
|
||||
|
||||
Ghidra's generic RV32 compiler specification, `riscv32-fp.cspec`, models the
|
||||
ilp32d ABI. Its default prototype passes `float` and `double` values in
|
||||
`fa0`–`fa7` and returns them in `fa0`, and it passes a structure of one or two
|
||||
floating-point members in floating-point registers. An ilp32 image passes all
|
||||
of these in integer registers. Measured on Ghidra 12.1.4 with an ilp32 build of
|
||||
the fixture in `tests/conformance/c/riscv-float-abi.c`, the stock model
|
||||
decompiles `rea_riscv_abi_pick(1.5f, 2.5f, sel)` as
|
||||
`rea_riscv_abi_pick(in_fa0,in_fa1,0x3fc00000)`.
|
||||
|
||||
### Decision record: D3, RISC-V float ABIs
|
||||
|
||||
Decided in [#7477](https://github.com/morluto/rea/issues/7477).
|
||||
|
||||
- **Choice.**
|
||||
- Admit little-endian ELF32 `ET_EXEC` images that declare the soft-float ABI
|
||||
(ilp32) through a new lane, `elf32-exec-le-soft-float-generic-v1`. Lane ids
|
||||
are built from their fields: `elf<class>-exec-le-<float ABI>-float-generic-v1`.
|
||||
- Keep the pinned `RISCV:LE:32:default` language and `gcc` compiler
|
||||
specification. After analysis, `ReaGhidraRiscvCallingConvention.java`
|
||||
derives the program-specific `__rea_ilp32` prototype from the program's
|
||||
default prototype as the [ilp32 policy](#ilp32) states. It installs the
|
||||
prototype through a Ghidra specification extension and assigns it, with
|
||||
dynamic storage, to every non-thunk function, every external function and
|
||||
every function definition. Thunks follow their targets. The script fails,
|
||||
and writes no report, if the derived prototype or any function still
|
||||
places a value in a floating-point register, or if the prototype does not
|
||||
pass a 16-byte value by reference.
|
||||
- One derivation serves every policy. A policy row says how it changes the
|
||||
default prototype's floating-point entries and preserved registers; every
|
||||
policy passes values of 9 bytes or more by reference.
|
||||
- The profile commits `riscv_calling_convention: ilp32-prototype-v1`. The
|
||||
bridge reports the policy and its function and definition counts, and REA
|
||||
requires that report exactly when the profile commits the policy.
|
||||
- Admission requires a `Tag_RISCV_arch` declaration with an RV32I base and
|
||||
only extensions that Ghidra 12.1.4's generic RV32 language implements with
|
||||
p-code ([ISA declarations](#isa-declarations)).
|
||||
- Soft-float profiles add `float_abi` and `arch` to `riscv_elf`. Double-float
|
||||
profiles keep their exact parameters, so their digests, sessions and
|
||||
snapshots are unchanged.
|
||||
- ilp32f keeps its [written policy](#ilp32f) and stays refused until its
|
||||
prototype is implemented and verified. Quad-float, RVE, TSO, and ELF64
|
||||
soft- or single-float images stay refused.
|
||||
- **Alternatives** (and what switching to each would take):
|
||||
- Pin the deprecated soft-float languages (`RISCV:LE:32:RV32IMC` and others,
|
||||
`riscv32.cspec`): REA's language catalog would have to read `old/`, the
|
||||
variants cover only I, IC and IMC, Ghidra marks them deprecated, and no
|
||||
stock specification models ilp32f.
|
||||
- Assign `__rea_ilp32` only to callables whose types use floating-point
|
||||
values, as the MIPS soft-float lane does: typed storage is the same, but
|
||||
prototypes Ghidra infers for untyped code would keep the ilp32d model. In
|
||||
an ilp32 image that contains F or D instructions, that model can still
|
||||
claim `fa0`. Switching is one predicate in the Java script and a new
|
||||
policy id.
|
||||
- Admit with a limitation and no correction: a reduced-capability contract
|
||||
that needs its own review, because arguments, returns and aggregates
|
||||
become unverified.
|
||||
- Keep refusing: the lane rows become refusals again.
|
||||
- Admit images without `Tag_RISCV_arch`, or refuse a list of known
|
||||
extensions instead of admitting a list: a declaration that REA cannot
|
||||
check would be treated as compatible. Widening the decoded set later is
|
||||
additive.
|
||||
- Add `float_abi` and `arch` to double-float profiles as well: one shape for
|
||||
every lane, but every existing RISC-V session and snapshot would be
|
||||
invalidated.
|
||||
- Ship ilp32 and ilp32f together (the question in #7477): ilp32f needs a
|
||||
float-and-integer structure rule that Ghidra's model lacks, so it ships
|
||||
separately with its own fixtures.
|
||||
- **Semantic owner and consumers.**
|
||||
- Owners:
|
||||
- `src/domain/riscvIsaDeclaration.ts` owns the float-ABI vocabulary of
|
||||
`e_flags` and the parsing of `.riscv.attributes` and `Tag_RISCV_arch`.
|
||||
- `GHIDRA_RISCV_LANES`, `GHIDRA_RISCV_CALLING_CONVENTIONS` and
|
||||
`GHIDRA_RISCV32_DECODED_EXTENSIONS` (`src/ghidra/GhidraRiscvProfile.ts`)
|
||||
own admission, lane ids and the policy table.
|
||||
- `bridge/ghidra/ReaGhidraRiscvCallingConvention.java` owns how each policy
|
||||
becomes a Ghidra prototype.
|
||||
- Consumers: target resolution, Ghidra provider admission, the analysis
|
||||
profile, the provider client and launcher, the session handshake, project
|
||||
history, the real-Ghidra lane and this page. Later work reuses the table:
|
||||
ilp32f adds a row, and RISC-V ESP images (F6) select a row through D5.
|
||||
- **Costs of changing it.**
|
||||
- **Code:** S. A policy or lane is one table row plus the Java derivation.
|
||||
- **Compatibility:** the soft-float lane id, `riscv_elf.float_abi`,
|
||||
`riscv_elf.arch` and `riscv_calling_convention` are persisted in sessions,
|
||||
snapshots and Evidence once released. A changed derivation ships as a new
|
||||
policy id, such as `ilp32-prototype-v2`, which invalidates soft-float
|
||||
snapshots only.
|
||||
- **Migration:** none for admitted targets; double-float profiles are pinned
|
||||
by tests. Previously refused soft-float targets become admitted.
|
||||
- **Verification:** rerun `npm run verify:ghidra:riscv`.
|
||||
|
||||
### ilp32
|
||||
|
||||
Policy `ilp32-prototype-v1`, prototype `__rea_ilp32`. XLEN is 32 bits and no
|
||||
floating-point register passes a value.
|
||||
|
||||
| Topic | psABI rule | `__rea_ilp32` |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Integer and pointer values | `a0`–`a7`, then the stack in 4-byte-aligned slots | The default integer and stack entries, unchanged |
|
||||
| `float` | One integer register, or a stack slot | The `fa0`–`fa7` entries and the `float` rule are removed, so a `float` takes the next integer register |
|
||||
| `double` and 8-byte values | A register pair with the low word in the lower register. Named arguments need no aligned pair. With one register left, the low word goes in `a7` and the high word on the stack | The default `join` rule with stack spill: Ghidra writes `a2:4,a1:4` for a pair and `Stack[0x0]:4,a7:4` for the split |
|
||||
| Aggregates up to 8 bytes | One or two integer registers, laid out as in memory, also when every member is floating-point | The homogeneous-float-aggregate rule is removed; the `join` rule applies |
|
||||
| Aggregates over 8 bytes | By reference: the caller passes the address of a copy | The default rule that converts structures and unions of 9 bytes or more to pointers |
|
||||
| Return values | As the first argument of the same type: `a0`, or `a0` and `a1`. Larger values go to a caller buffer whose address is an implicit first argument in `a0` | The `fa0`/`fa1` return entries and floating-point rules are removed; Ghidra's hidden-return fallback supplies the pointer in `a0` |
|
||||
| Variadic arguments | The integer convention. A variadic argument with 8-byte alignment and size takes an aligned (even-numbered) register pair, or the stack; after the first stack argument, the rest go on the stack | The default variadic rule (`join` with alignment), unchanged |
|
||||
| Preserved registers | `s0`–`s11` and `sp`; `gp` and `tp` are not allocated. No floating-point register is preserved | The default preserved list without `fs0`–`fs11`, which join the killed-by-call list |
|
||||
| Scalars over 8 bytes | By reference (`long double` is 16 bytes), and returned through a caller buffer | A rule before the final join converts any value of 9 bytes or more to a pointer; a return that size already goes to a caller buffer |
|
||||
|
||||
Ghidra recovers the arguments of a variadic call from the registers the caller
|
||||
writes, not from their types. A padding register that still holds an earlier
|
||||
value can appear as an argument, and arguments after an unwritten register are
|
||||
omitted.
|
||||
|
||||
### ilp32f
|
||||
|
||||
Written policy for `EF_RISCV_FLOAT_ABI_SINGLE` on ELF32. REA does not admit it
|
||||
yet. ABI_FLEN is 32 bits.
|
||||
|
||||
| Topic | psABI rule |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `float` | `fa0`–`fa7` while one is free, then the integer convention |
|
||||
| `double` and other values wider than 32 bits | The integer convention, as in ilp32 |
|
||||
| Structure of one or two floats (after flattening) | One floating-point register per member when enough are free, otherwise the integer convention |
|
||||
| Structure of one float and one integer, in either order | One floating-point and one integer register when both are free, otherwise the integer convention |
|
||||
| Unions | Never flattened; the integer convention |
|
||||
| Return values | As the first argument: `fa0`, `fa0` and `fa1`, or `fa0` and `a0` |
|
||||
| Variadic arguments | The integer convention, as in ilp32 |
|
||||
| Preserved registers | `s0`–`s11`, `sp`, and `fs0`–`fs11` for values of 32 bits or fewer |
|
||||
|
||||
The planned prototype limits the default floating-point entries to 4 bytes and
|
||||
keeps the shared rule that passes values of 9 bytes or more by reference.
|
||||
Observed on Ghidra 12.1.4, a `double` then takes an integer pair and a structure
|
||||
of two floats takes `fa0` and `fa1`. Ghidra's model has no rule for a structure
|
||||
of one float and one integer: it joins `struct { float; int; }` into `a1:a0`
|
||||
instead of `fa0` and `a0`. The ilp32f policy needs that rule and its own
|
||||
fixtures before it is admitted.
|
||||
|
||||
### ilp32d and lp64d
|
||||
|
||||
The double-float lanes use Ghidra's stock `riscv32-fp.cspec` and
|
||||
`riscv64-fp.cspec` models unchanged. This page does not verify them. They share
|
||||
the float-and-integer structure gap above.
|
||||
|
||||
## ISA declarations
|
||||
|
||||
The target reader finds the one `SHT_RISCV_ATTRIBUTES` section and reads
|
||||
`Tag_RISCV_arch`, such as `rv32i2p1_m2p0_c2p0_zicsr2p0_zifencei2p0`. It
|
||||
retains the string, the base and the extension names, or an unavailable reason
|
||||
when the section is malformed, declares other vendors or scopes, or uses a
|
||||
non-canonical string. A missing section or tag is recorded as absent.
|
||||
|
||||
Calling-convention lanes admit an image only when it declares an RV32I base
|
||||
and only extensions that Ghidra 12.1.4's generic RV32 language implements with
|
||||
p-code: `m`, `a`, `f`, `d`, `c`, `zicsr`, `zifencei`, and the subset names
|
||||
toolchains record, `zmmul`, `zaamo`, `zalrsc`, `zca`, `zcf` and `zcd`. Any
|
||||
other extension is refused. In particular:
|
||||
|
||||
- Ghidra 12.1.4 decodes no Zcb, Zcmp or Zcmt instruction.
|
||||
- Its bit-manipulation, cryptography and packed-SIMD files follow draft
|
||||
encodings and mark nearly every instruction unimplemented.
|
||||
- Vendor extensions, such as Espressif's `xesploop`, `xespv` and `xespdsp`,
|
||||
decode only as custom-opcode placeholders.
|
||||
|
||||
ESP-IDF builds ESP32-C2 and C3 images as `rv32imc_zicsr_zifencei` and ESP32-C5,
|
||||
C6, C61, H2 and H21 images as `rv32imac_zicsr_zifencei_zaamo_zalrsc`, all ilp32.
|
||||
Those base strings are admitted. Builds that add `_zba_zbb_zbs`,
|
||||
`_zcb_zcmp_zcmt` or an `xesp` extension are refused.
|
||||
|
||||
## Evidence
|
||||
|
||||
The lane identity is `elf32-exec-le-double-float-generic-v1` or
|
||||
`elf64-exec-le-double-float-generic-v1`, and `riscv_elf` retains `elf_class`,
|
||||
`byte_order`, `type`, and the raw `flags` alongside the read-only booleans
|
||||
`float_abi_double`, `rvc_declared`, `rve_declared`, and `tso_declared`. Those
|
||||
fields report declarations the header makes; none of them is evidence of the
|
||||
original compiler, of instruction conformance, or of a floating-point calling
|
||||
convention beyond the ABI the pinned language models.
|
||||
The lane identity is one of the lane ids above, and `riscv_elf` retains
|
||||
`elf_class`, `byte_order`, `type`, and the raw `flags` alongside the read-only
|
||||
booleans `float_abi_double`, `rvc_declared`, `rve_declared`, and
|
||||
`tso_declared`. Soft-float profiles also retain `float_abi: soft`, the declared
|
||||
`arch` string, and `riscv_calling_convention: ilp32-prototype-v1`. Those fields
|
||||
report declarations the header and attributes make, and the policy REA applied;
|
||||
none of them is evidence of the original compiler or of instruction
|
||||
conformance. Session Evidence states the prototype and how many functions and
|
||||
definitions use it.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
@@ -76,9 +268,9 @@ tree ships no host decompiler binary, so `Decompiler/os/<platform>/decompile` an
|
||||
[installation](installation.md#ghidra).
|
||||
|
||||
Run `npm run verify:ghidra:riscv` with an existing Ghidra 12.1.4 installation,
|
||||
JDK 21+, and a RISC-V-capable Clang, LLD and LLVM readelf. The verifier acquires
|
||||
no tools and never executes target code. Select installed tools explicitly when
|
||||
they are not on `PATH`:
|
||||
JDK 21+, and a RISC-V-capable Clang, LLD, LLVM readelf and LLVM objdump. The
|
||||
verifier acquires no tools and never executes target code. Select installed
|
||||
tools explicitly when they are not on `PATH`:
|
||||
|
||||
```sh
|
||||
GHIDRA_INSTALL_DIR=/path/to/ghidra_12.1.4_PUBLIC \
|
||||
@@ -86,20 +278,52 @@ JAVA_HOME=/path/to/jdk-21 \
|
||||
REA_RISCV_CLANG=/path/to/llvm/bin/clang \
|
||||
REA_RISCV_LLD=/path/to/lld/bin/ld.lld \
|
||||
REA_RISCV_READELF=/path/to/llvm/bin/llvm-readelf \
|
||||
REA_RISCV_OBJDUMP=/path/to/llvm/bin/llvm-objdump \
|
||||
npm run verify:ghidra:riscv
|
||||
```
|
||||
|
||||
The lane builds the committed freestanding C and assembly sources into RV32 and
|
||||
RV64 `ET_EXEC` fixtures. Independent LLVM readelf observations establish ELF
|
||||
declarations and symbol addresses. Public CLI and stdio MCP workflows verify
|
||||
function enumeration, decompilation, pinned language/compiler identity, decoded
|
||||
probe bytes, global data, and direct calls. It validates advertised MCP output
|
||||
schemas and both JSON representations, saves and reopens snapshots, rejects
|
||||
header-mutated soft-float and unsupported high-bit declarations, and checks
|
||||
unchanged source bytes and owned cleanup. Failures retain the diagnostic
|
||||
workspace; successful runs remove it and report the observed process lineage.
|
||||
The lane builds four `ET_EXEC` fixtures from committed freestanding C and
|
||||
assembly sources:
|
||||
|
||||
Module tests cover admission and malformed headers. Call-graph completeness,
|
||||
string inventories, dynamic linkage, Andes or RVE/TSO images, and big-endian
|
||||
targets are **not** established by this lane. `.riscv.attributes` ISA extensions
|
||||
are not inspected; the generic language pin remains an analysis convention.
|
||||
- RV32 and RV64 double-float builds (`rv{32,64}imafdc`) of `riscv.c`.
|
||||
- An RV32 soft-float build of `riscv-float-abi.c` with ESP32-C3's
|
||||
`-march=rv32imc_zicsr_zifencei -mabi=ilp32`, typed with DWARF.
|
||||
- The same objects linked with `--strip-all`. Every allocated section keeps
|
||||
its address and bytes, so the typed build's symbols locate its functions.
|
||||
|
||||
Independent LLVM readelf observations establish ELF declarations,
|
||||
`Tag_RISCV_arch` and symbol addresses. Public CLI and stdio MCP workflows verify
|
||||
function enumeration, decompilation, pinned language/compiler identity, the
|
||||
committed lane and policy, decoded probe bytes, global data, and direct calls.
|
||||
They validate advertised MCP output schemas and both JSON representations, save
|
||||
and reopen snapshots, and check unchanged source bytes and owned cleanup.
|
||||
|
||||
For the soft-float builds the lane also checks:
|
||||
|
||||
- `fence.i` and `csrr` decode, as ESP32-C3's Zifencei and Zicsr declare.
|
||||
- An LLVM objdump reading of `rea_riscv_abi_caller` finds each call-site word
|
||||
where the [ilp32 policy](#ilp32) puts it: an unaligned `double` in `a1`/`a2`,
|
||||
the `a7`/stack split, two floats in an integer pair, a large structure and a
|
||||
`long double` passed by reference (the pointed-to copies hold the source
|
||||
values), aligned variadic pairs, and an indirect call.
|
||||
- `inspect_native_api` reports `__rea_ilp32` with exactly that storage for the
|
||||
typed build. The stripped build has one integer word per parameter location
|
||||
and returns in the typed build's registers, or in none when the result goes
|
||||
through caller memory.
|
||||
- The caller's pseudocode passes the source constants through that storage and
|
||||
names no floating-point register.
|
||||
- Retained Evidence states the prototype outcome; the stripped build has no
|
||||
function definitions.
|
||||
|
||||
Header-mutated copies check admission. The RV32 double-float image relabelled as
|
||||
soft-float (`e_flags` `0x1`), refused before this lane, is admitted through the
|
||||
soft-float lane over CLI and MCP. The soft-float image relabelled as
|
||||
single-float, RV64 soft-float, and unsupported high bits are refused with their
|
||||
reasons. Failures retain the diagnostic workspace; successful runs remove it and
|
||||
report the observed process lineage.
|
||||
|
||||
Module tests cover admission, malformed headers and attribute sections, the
|
||||
handshake, and the conformance checks against recorded LLVM and Ghidra output.
|
||||
Call-graph completeness, string inventories, dynamic linkage, Andes or RVE/TSO
|
||||
images, big-endian targets, ilp32f images, and decoding beyond the probes are
|
||||
**not** established by this lane.
|
||||
+1
-1
@@ -1061,7 +1061,7 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
|
||||
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:seeds` | POSIX; raw x86 fixture; seed ordering, BSS labels, analyzer readback, not-applicable and unknown analyzer names | Ghidra 12.1.4 and full JDK 21 |
|
||||
| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:riscv` | Linux/macOS x64/arm64 host; source-owned RV32/RV64 ELF through CLI and MCP | RISC-V Clang, LLD, LLVM readelf, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:riscv` | Linux/macOS x64/arm64 host; source-owned RV32/RV64 double-float and RV32 ilp32 ELF through CLI and MCP | RISC-V Clang, LLD, LLVM readelf and objdump, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; signed/unsigned byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"bridge/ghidra/ReaGhidraAnalysisOptions.java",
|
||||
"bridge/ghidra/ReaGhidraNoReturnFix.java",
|
||||
"bridge/ghidra/ReaGhidraMipsSoftFloat.java",
|
||||
"bridge/ghidra/ReaGhidraRiscvCallingConvention.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareRaw.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareDol.java",
|
||||
"bridge/android/ReaJadxBridge.java",
|
||||
|
||||
@@ -23,7 +23,17 @@ import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
|
||||
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
|
||||
import { buildRiscvFixture } from "../tests/conformance/ghidra/riscv-fixture.mjs";
|
||||
import {
|
||||
buildRiscvFixture,
|
||||
stripRiscvFixture,
|
||||
} from "../tests/conformance/ghidra/riscv-fixture.mjs";
|
||||
import {
|
||||
RISCV_ILP32_ABI,
|
||||
assertRiscvIlp32CallSites,
|
||||
assertRiscvIlp32Caller,
|
||||
assertRiscvIlp32Storage,
|
||||
inspectRiscvCallSites,
|
||||
} from "../tests/conformance/ghidra/riscv-checks.mjs";
|
||||
import {
|
||||
mcpTextValue,
|
||||
requireMcpLifecycleResult,
|
||||
@@ -82,6 +92,29 @@ transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
|
||||
/** The profile a fixture's lane must commit, from independent declarations. */
|
||||
const expectedLane = (fixture) => {
|
||||
const soft = fixture.riscv.flags === 1;
|
||||
return {
|
||||
lane: `elf${fixture.riscv.elfClass}-exec-le-${soft ? "soft" : "double"}-float-generic-v1`,
|
||||
callingConvention: soft ? "ilp32-prototype-v1" : undefined,
|
||||
riscvElf: {
|
||||
elf_class: fixture.riscv.elfClass,
|
||||
byte_order: "little",
|
||||
type: 2,
|
||||
flags: fixture.riscv.flags,
|
||||
float_abi_double: !soft,
|
||||
...(soft ? { float_abi: "soft", arch: fixture.riscv.isa.arch } : {}),
|
||||
rvc_declared: true,
|
||||
rve_declared: false,
|
||||
tso_declared: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const OUTCOME =
|
||||
/RISC-V calling-convention outcome: [1-9]\d* functions and (\d+) function definitions use __rea_ilp32\./u;
|
||||
|
||||
const reports = [];
|
||||
let failure;
|
||||
try {
|
||||
@@ -101,41 +134,55 @@ try {
|
||||
: undefined;
|
||||
validators.set(tool.name, existing ?? ajv.compile(tool.outputSchema));
|
||||
}
|
||||
for (const bits of [32, 64]) {
|
||||
const fixture = await buildRiscvFixture(workspace, bits);
|
||||
|
||||
const typedSoft = await buildRiscvFixture(workspace, 32, "soft");
|
||||
const callSites = await inspectRiscvCallSites(typedSoft.path);
|
||||
assertRiscvIlp32CallSites(callSites.calls);
|
||||
const fixtures = [
|
||||
await buildRiscvFixture(workspace, 32),
|
||||
await buildRiscvFixture(workspace, 64),
|
||||
typedSoft,
|
||||
await stripRiscvFixture(workspace, typedSoft),
|
||||
];
|
||||
for (const fixture of fixtures) {
|
||||
const bits = fixture.riscv.elfClass;
|
||||
const soft = fixture.riscv.flags === 1;
|
||||
const stripped = fixture.stripped === true;
|
||||
const expected = expectedLane(fixture);
|
||||
const resolved = await parseBinaryTarget(fixture.path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
assert.equal(resolved.value.architecture, "riscv");
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
assert.deepEqual(resolved.value.riscv, fixture.riscv);
|
||||
const expectedLanguage = `RISCV:LE:${bits}:default`;
|
||||
const assertProfile = (profile) => {
|
||||
const assertProfile = (profile, want = expected) => {
|
||||
assert.equal(profile.provider.id, "ghidra");
|
||||
assert.equal(profile.provider.version, installation.providerVersion);
|
||||
const parameters = profile.parameters;
|
||||
assert.equal(parameters.language_id, expectedLanguage);
|
||||
assert.equal(parameters.compiler_spec_id, "gcc");
|
||||
assert.equal(parameters.language_selection, "provider-pinned-v1");
|
||||
assert.equal(
|
||||
parameters.riscv_support_lane,
|
||||
`elf${bits}-exec-le-double-float-generic-v1`,
|
||||
);
|
||||
assert.deepEqual(parameters.riscv_elf, {
|
||||
elf_class: bits,
|
||||
byte_order: "little",
|
||||
type: 2,
|
||||
flags: 5,
|
||||
float_abi_double: true,
|
||||
rvc_declared: true,
|
||||
rve_declared: false,
|
||||
tso_declared: false,
|
||||
});
|
||||
assert.equal(parameters.riscv_support_lane, want.lane);
|
||||
assert.equal(parameters.riscv_calling_convention, want.callingConvention);
|
||||
assert.deepEqual(parameters.riscv_elf, want.riscvElf);
|
||||
};
|
||||
const assertEvidence = (evidence) => {
|
||||
assert.equal(evidence.provider.id, "ghidra");
|
||||
assert.equal(evidence.subject.architecture, "riscv");
|
||||
assert.equal(evidence.subject.digest.sha256, fixture.sha256);
|
||||
assertProfile(evidence.analysis_profile);
|
||||
// Composed workflows such as inspect_native_api commit the Ghidra
|
||||
// profile as their upstream profile. The residual-unknown registry
|
||||
// records what they could not establish, such as the types of a
|
||||
// stripped function's parameters, without a profile of its own.
|
||||
if (evidence.provider.id === "rea-workflow")
|
||||
assertProfile(
|
||||
evidence.analysis_profile.parameters.upstream_analysis_profile,
|
||||
);
|
||||
else if (evidence.provider.id === "rea-unknown-registry")
|
||||
assert.equal(evidence.analysis_profile, null);
|
||||
else {
|
||||
assert.equal(evidence.provider.id, "ghidra");
|
||||
assertProfile(evidence.analysis_profile);
|
||||
}
|
||||
};
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool(
|
||||
@@ -176,13 +223,32 @@ try {
|
||||
assert.equal(source.modified_sha256, fixture.sha256);
|
||||
}
|
||||
const procedures = await call("list_procedures");
|
||||
for (const name of [
|
||||
"rea_riscv_entry",
|
||||
"rea_riscv_leaf",
|
||||
"rea_riscv_float",
|
||||
"rea_riscv_probe",
|
||||
]) {
|
||||
const procedure = procedures.find((value) => value.value === name);
|
||||
const names = soft
|
||||
? [
|
||||
"rea_riscv_entry",
|
||||
"rea_riscv_probe",
|
||||
"rea_riscv_isa_probe",
|
||||
"rea_riscv_abi_caller",
|
||||
...Object.keys(RISCV_ILP32_ABI),
|
||||
]
|
||||
: [
|
||||
"rea_riscv_entry",
|
||||
"rea_riscv_leaf",
|
||||
"rea_riscv_float",
|
||||
"rea_riscv_probe",
|
||||
];
|
||||
for (const name of [...names, "rea_riscv_global"])
|
||||
assert.ok(
|
||||
fixture.symbols[name],
|
||||
`${name} missing from the readelf symbols`,
|
||||
);
|
||||
for (const name of names) {
|
||||
// A stripped image has no names; the typed twin's addresses locate it.
|
||||
const procedure = procedures.find((value) =>
|
||||
stripped
|
||||
? BigInt(value.address) === BigInt(fixture.symbols[name])
|
||||
: value.value === name,
|
||||
);
|
||||
assert.ok(procedure, `${name} missing from Ghidra inventory`);
|
||||
assert.equal(BigInt(procedure.address), BigInt(fixture.symbols[name]));
|
||||
}
|
||||
@@ -203,6 +269,22 @@ try {
|
||||
.toString("hex"),
|
||||
probe.bytes,
|
||||
);
|
||||
const isaProbe = [];
|
||||
if (soft) {
|
||||
// ESP32-C3's -march declares Zifencei and Zicsr; both must decode.
|
||||
for (const [offset, bytes, mnemonic] of [
|
||||
[0n, "0f100000", /^fence\.i$/iu],
|
||||
[4n, "f32200b0", /^csrr/iu],
|
||||
]) {
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: `0x${(BigInt(fixture.symbols.rea_riscv_isa_probe) + offset).toString(16)}`,
|
||||
});
|
||||
assert.equal(instruction.status, "decoded");
|
||||
assert.equal(instruction.bytes, bytes);
|
||||
assert.match(instruction.mnemonic, mnemonic);
|
||||
isaProbe.push(instruction.mnemonic);
|
||||
}
|
||||
}
|
||||
const global = await call("read_bytes", {
|
||||
address: fixture.symbols.rea_riscv_global,
|
||||
length: 4,
|
||||
@@ -211,11 +293,14 @@ try {
|
||||
const callees = await call("procedure_callees", {
|
||||
procedure: fixture.symbols.rea_riscv_entry,
|
||||
});
|
||||
for (const name of ["rea_riscv_leaf", "rea_riscv_probe"])
|
||||
for (const name of soft
|
||||
? ["rea_riscv_abi_caller", "rea_riscv_isa_probe", "rea_riscv_probe"]
|
||||
: ["rea_riscv_leaf", "rea_riscv_probe"])
|
||||
assert.ok(
|
||||
callees.some(
|
||||
(address) => BigInt(address) === BigInt(fixture.symbols[name]),
|
||||
),
|
||||
`${name} missing from the entry's callees`,
|
||||
);
|
||||
const dossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", {
|
||||
@@ -224,60 +309,50 @@ try {
|
||||
}),
|
||||
);
|
||||
assert.ok(dossier.pseudocode.trim().length > 0);
|
||||
const floatCode = await call("procedure_pseudo_code", {
|
||||
procedure: fixture.symbols.rea_riscv_float,
|
||||
});
|
||||
assert.ok(typeof floatCode === "string" && floatCode.trim().length > 0);
|
||||
assert.match(floatCode, /\bdouble\b/u);
|
||||
|
||||
const refusals = [];
|
||||
for (const [flags, reason] of [
|
||||
[1, "EF_RISCV_FLOAT_ABI_DOUBLE"],
|
||||
[0x80000005, "(0x80000000)"],
|
||||
]) {
|
||||
// Header-mutated copies test admission; they are not new compiler claims.
|
||||
const rejectedPath = join(
|
||||
workspace,
|
||||
`rv${bits}-flags-${flags.toString(16)}.elf`,
|
||||
);
|
||||
const rejectedBytes = Buffer.from(sourceBytes);
|
||||
rejectedBytes.writeUInt32LE(flags, bits === 32 ? 36 : 48);
|
||||
await writeFile(rejectedPath, rejectedBytes);
|
||||
const reply = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: rejectedPath, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
const error = requireMcpToolError(reply);
|
||||
assert.ok(JSON.stringify(error).includes(reason), JSON.stringify(error));
|
||||
await assert.rejects(
|
||||
execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
rejectedPath,
|
||||
"rea_riscv_entry",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
env,
|
||||
timeout: 60000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
},
|
||||
),
|
||||
(cause) =>
|
||||
Number.isInteger(cause.code) &&
|
||||
cause.code !== 0 &&
|
||||
`${cause.stdout}\n${cause.stderr}`.includes(reason),
|
||||
);
|
||||
refusals.push({ flags, reason });
|
||||
if (!stripped) {
|
||||
const floatCode = await call("procedure_pseudo_code", {
|
||||
procedure:
|
||||
fixture.symbols[soft ? "rea_riscv_abi_wide" : "rea_riscv_float"],
|
||||
});
|
||||
assert.ok(typeof floatCode === "string" && floatCode.trim().length > 0);
|
||||
assert.match(floatCode, /\bdouble\b/u);
|
||||
}
|
||||
const snapshotPath = join(workspace, `rv${bits}.snapshot.json`);
|
||||
let abi;
|
||||
if (soft) {
|
||||
const boundaries = {};
|
||||
for (const name of Object.keys(RISCV_ILP32_ABI))
|
||||
boundaries[name] = (
|
||||
await call("inspect_native_api", { procedure: fixture.symbols[name] })
|
||||
).boundary;
|
||||
assertRiscvIlp32Storage(boundaries, { stripped });
|
||||
const caller = functionDossierSchema.parse(
|
||||
await call("analyze_function", {
|
||||
procedure: fixture.symbols.rea_riscv_abi_caller,
|
||||
detail: "complete",
|
||||
}),
|
||||
);
|
||||
assertRiscvIlp32Caller(caller.pseudocode, fixture.symbols, {
|
||||
stripped,
|
||||
});
|
||||
abi = {
|
||||
storage: Object.fromEntries(
|
||||
Object.entries(boundaries).map(([name, boundary]) => [
|
||||
name,
|
||||
{
|
||||
return: boundary.return_type.storage,
|
||||
parameters: boundary.parameters.map(
|
||||
(parameter) => parameter.storage,
|
||||
),
|
||||
},
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const snapshotPath = join(
|
||||
workspace,
|
||||
`${fixture.path.split("/").at(-1)}.snapshot.json`,
|
||||
);
|
||||
const saved = await call("close_binary", { snapshot_path: snapshotPath });
|
||||
assert.ok(saved.evidence_records > 0);
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
@@ -288,6 +363,19 @@ try {
|
||||
assertProfile(snapshot.binding.analysis_profile);
|
||||
for (const record of snapshot.evidence_bundle.records)
|
||||
assertEvidence(parseEvidence(record));
|
||||
const outcomes = snapshot.evidence_bundle.records
|
||||
.flatMap((record) => parseEvidence(record).limitations)
|
||||
.map((limitation) => OUTCOME.exec(limitation))
|
||||
.filter((match) => match !== null);
|
||||
assert.equal(
|
||||
outcomes.length > 0,
|
||||
soft,
|
||||
"ilp32 prototype outcome missing from, or unexpected in, retained Evidence",
|
||||
);
|
||||
// DWARF supplies the typed build's function definitions; stripping removes them.
|
||||
if (soft)
|
||||
for (const match of outcomes)
|
||||
assert.equal(Number(match[1]) > 0, !stripped, match[0]);
|
||||
await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
@@ -297,8 +385,11 @@ try {
|
||||
await call("close_binary");
|
||||
|
||||
for (const [command, arguments_] of [
|
||||
["search", ["rea_riscv_", "--kind", "procedures"]],
|
||||
["function", ["rea_riscv_entry"]],
|
||||
...(stripped ? [] : [["search", ["rea_riscv_", "--kind", "procedures"]]]),
|
||||
[
|
||||
"function",
|
||||
[stripped ? fixture.symbols.rea_riscv_entry : "rea_riscv_entry"],
|
||||
],
|
||||
]) {
|
||||
const cli = await execute(
|
||||
process.execPath,
|
||||
@@ -316,12 +407,7 @@ try {
|
||||
const evidence = parseEvidence(JSON.parse(cli.stdout));
|
||||
assertEvidence(evidence);
|
||||
if (command === "search")
|
||||
for (const name of [
|
||||
"rea_riscv_entry",
|
||||
"rea_riscv_leaf",
|
||||
"rea_riscv_float",
|
||||
"rea_riscv_probe",
|
||||
])
|
||||
for (const name of names)
|
||||
assert.ok(
|
||||
evidence.normalized_result.some((value) => value.value === name),
|
||||
);
|
||||
@@ -333,6 +419,92 @@ try {
|
||||
assert.ok(cliDossier.pseudocode.trim().length > 0);
|
||||
}
|
||||
}
|
||||
const headers = [];
|
||||
// Header-mutated copies test admission; they are not new compiler claims.
|
||||
for (const [flags, outcome] of stripped
|
||||
? []
|
||||
: soft
|
||||
? [
|
||||
[0x3, "single-float ABI (ilp32f)"],
|
||||
[0x80000001, "(0x80000000)"],
|
||||
]
|
||||
: bits === 32
|
||||
? [
|
||||
[0x1, "admitted"],
|
||||
[0x80000005, "(0x80000000)"],
|
||||
]
|
||||
: [
|
||||
[0x1, "soft-float ABI (lp64)"],
|
||||
[0x80000005, "(0x80000000)"],
|
||||
]) {
|
||||
const mutatedPath = join(
|
||||
workspace,
|
||||
`rv${bits}${soft ? "-soft" : ""}-flags-${flags.toString(16)}.elf`,
|
||||
);
|
||||
const mutatedBytes = Buffer.from(sourceBytes);
|
||||
mutatedBytes.writeUInt32LE(flags, bits === 32 ? 36 : 48);
|
||||
await writeFile(mutatedPath, mutatedBytes);
|
||||
const reply = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: mutatedPath, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
const cliArguments = [
|
||||
entrypoint,
|
||||
"function",
|
||||
mutatedPath,
|
||||
"rea_riscv_entry",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
];
|
||||
const cliOptions = { env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 };
|
||||
if (outcome === "admitted") {
|
||||
// The double-float image now declares ilp32: refused before, admitted
|
||||
// now through the soft-float lane with the image's own ISA string.
|
||||
assert.equal(
|
||||
requireMcpLifecycleResult(reply, "open_binary").sha256,
|
||||
createHash("sha256").update(mutatedBytes).digest("hex"),
|
||||
);
|
||||
requireMcpLifecycleResult(
|
||||
await client.callTool(
|
||||
{ name: "close_binary", arguments: {} },
|
||||
{ timeout: 240000 },
|
||||
),
|
||||
"close_binary",
|
||||
);
|
||||
const want = {
|
||||
lane: "elf32-exec-le-soft-float-generic-v1",
|
||||
callingConvention: "ilp32-prototype-v1",
|
||||
riscvElf: {
|
||||
...expected.riscvElf,
|
||||
flags,
|
||||
float_abi_double: false,
|
||||
float_abi: "soft",
|
||||
arch: fixture.riscv.isa.arch,
|
||||
},
|
||||
};
|
||||
const cli = await execute(process.execPath, cliArguments, cliOptions);
|
||||
assertProfile(
|
||||
parseEvidence(JSON.parse(cli.stdout)).analysis_profile,
|
||||
want,
|
||||
);
|
||||
headers.push({ flags, outcome, lane: want.lane });
|
||||
continue;
|
||||
}
|
||||
const error = requireMcpToolError(reply);
|
||||
assert.ok(JSON.stringify(error).includes(outcome), JSON.stringify(error));
|
||||
await assert.rejects(
|
||||
execute(process.execPath, cliArguments, cliOptions),
|
||||
(cause) =>
|
||||
Number.isInteger(cause.code) &&
|
||||
cause.code !== 0 &&
|
||||
`${cause.stdout}\n${cause.stderr}`.includes(outcome),
|
||||
);
|
||||
headers.push({ flags, outcome: "refused", reason: outcome });
|
||||
}
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(fixture.path))
|
||||
@@ -340,16 +512,38 @@ try {
|
||||
fixture.sha256,
|
||||
);
|
||||
reports.push({
|
||||
...fixture,
|
||||
path: fixture.path,
|
||||
sha256: fixture.sha256,
|
||||
riscv: fixture.riscv,
|
||||
stripped,
|
||||
toolchain: fixture.toolchain,
|
||||
sources: fixture.sources,
|
||||
language: expectedLanguage,
|
||||
lane: expected.lane,
|
||||
calling_convention: expected.callingConvention ?? null,
|
||||
procedures: procedures.length,
|
||||
refusals,
|
||||
headers,
|
||||
...(isaProbe.length === 0 ? {} : { isa_probe: isaProbe }),
|
||||
...(abi === undefined
|
||||
? {}
|
||||
: {
|
||||
abi,
|
||||
prototype_outcome: outcomes.map((match) => match[0]),
|
||||
}),
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
snapshot_replay: true,
|
||||
original_unchanged: true,
|
||||
});
|
||||
}
|
||||
reports.push({
|
||||
call_sites: {
|
||||
fixture: typedSoft.path,
|
||||
command: callSites.command,
|
||||
version: callSites.version,
|
||||
calls: callSites.calls.map((value) => value.target),
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,11 @@ import type {
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { readMipsElfMetadata } from "./MipsElfMetadata.js";
|
||||
import { validateElfDeclaredRanges } from "../domain/elfDeclaredRanges.js";
|
||||
import {
|
||||
validateElfDeclaredRanges,
|
||||
type ElfDeclarationRead,
|
||||
} from "../domain/elfDeclaredRanges.js";
|
||||
import { readRiscvIsaDeclaration } from "../domain/riscvIsaDeclaration.js";
|
||||
import {
|
||||
resolveAppleBundleExecutable,
|
||||
type ResolvedAppBundle,
|
||||
@@ -1107,8 +1111,26 @@ const readElfMetadata = async (
|
||||
const { fileSize, hostArchitecture, signal } = options;
|
||||
const parsed = parseExecutableHeader(bytes, hostArchitecture, fileSize);
|
||||
if (!parsed.ok || parsed.value.format !== "elf") return parsed;
|
||||
const ranges = await readElfDeclaredRanges(handle, bytes, fileSize, signal);
|
||||
const ranges = await readElfDeclarations(
|
||||
handle,
|
||||
bytes,
|
||||
validateElfDeclaredRanges(bytes, fileSize),
|
||||
signal,
|
||||
);
|
||||
if (!ranges.ok) return ranges;
|
||||
if (parsed.value.architecture === "riscv")
|
||||
return ok({
|
||||
...parsed.value,
|
||||
riscv: {
|
||||
...parsed.value.riscv,
|
||||
isa: await readElfDeclarations(
|
||||
handle,
|
||||
bytes,
|
||||
readRiscvIsaDeclaration(bytes, fileSize),
|
||||
signal,
|
||||
),
|
||||
},
|
||||
});
|
||||
if (parsed.value.architecture !== "mips" || parsed.value.mips.elfClass !== 32)
|
||||
return parsed;
|
||||
const declarations = await readMipsElfMetadata(
|
||||
@@ -1133,15 +1155,18 @@ const readElfMetadata = async (
|
||||
});
|
||||
};
|
||||
|
||||
/** Borrow the admitted descriptor; every request is at most one 64 KiB table chunk. */
|
||||
const readElfDeclaredRanges = async (
|
||||
/**
|
||||
* Satisfy one ELF declaration reader from the admitted descriptor. Every
|
||||
* request is at most one 64 KiB chunk, and the reader consumes each chunk
|
||||
* before its next request reuses the window.
|
||||
*/
|
||||
const readElfDeclarations = async <T>(
|
||||
handle: FileHandle,
|
||||
prefix: Buffer,
|
||||
fileSize: number,
|
||||
steps: Generator<ElfDeclarationRead, T, Buffer>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<null, string>> => {
|
||||
const ranges = validateElfDeclaredRanges(prefix, fileSize);
|
||||
let next = ranges.next();
|
||||
): Promise<T> => {
|
||||
let next = steps.next();
|
||||
let window: Buffer | undefined;
|
||||
while (!next.done) {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
@@ -1166,7 +1191,7 @@ const readElfDeclaredRanges = async (
|
||||
}
|
||||
bytes = bytes.subarray(0, length);
|
||||
}
|
||||
next = ranges.next(bytes);
|
||||
next = steps.next(bytes);
|
||||
}
|
||||
return next.value;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
HeaderArchitecture,
|
||||
MipsElfMetadata,
|
||||
PeCodeViewRecord,
|
||||
RiscvElfMetadata,
|
||||
UniversalMachOSlice,
|
||||
UniversalMachOSliceIdentity,
|
||||
} from "./binaryTargetTypes.js";
|
||||
@@ -47,7 +48,7 @@ export type ExecutableMetadata =
|
||||
readonly format: "elf";
|
||||
readonly architecture: "riscv";
|
||||
readonly availableArchitectures: readonly "riscv"[];
|
||||
readonly riscv: ElfFamilyMetadata;
|
||||
readonly riscv: RiscvElfMetadata;
|
||||
}
|
||||
| {
|
||||
readonly format: "elf";
|
||||
|
||||
@@ -87,6 +87,32 @@ export interface MipsElfMetadata extends ElfFamilyMetadata {
|
||||
| { readonly status: "unavailable"; readonly reason: string };
|
||||
}
|
||||
|
||||
/** Raw ELF facts; family recognition does not establish ISA or ABI support. */
|
||||
export interface RiscvElfMetadata extends ElfFamilyMetadata {
|
||||
/** Undefined until `.riscv.attributes` is inspected. */
|
||||
readonly isa?: RiscvIsaDeclaration;
|
||||
}
|
||||
|
||||
/** The ISA an image declares in `.riscv.attributes`; not proof of every instruction. */
|
||||
export type RiscvIsaDeclaration =
|
||||
| {
|
||||
readonly status: "declared";
|
||||
/** `Tag_RISCV_arch` exactly as declared. */
|
||||
readonly arch: string;
|
||||
readonly xlen: 32 | 64;
|
||||
readonly base: "i" | "e";
|
||||
/** Extension names in declaration order, without versions. */
|
||||
readonly extensions: readonly string[];
|
||||
}
|
||||
/** Every section header was examined and none declares `Tag_RISCV_arch`. */
|
||||
| { readonly status: "absent" }
|
||||
| {
|
||||
readonly status: "unavailable";
|
||||
readonly reason: string;
|
||||
/** The declared string, when one was read but not understood. */
|
||||
readonly arch?: string;
|
||||
};
|
||||
|
||||
/** Declared fields of the 24-byte MIPS ABI flags record, not inferred semantics. */
|
||||
export interface MipsAbiFlags {
|
||||
readonly version: number;
|
||||
@@ -164,8 +190,7 @@ export type BinaryTarget =
|
||||
readonly format: "elf";
|
||||
readonly architecture: "riscv";
|
||||
readonly availableArchitectures: readonly "riscv"[];
|
||||
/** ISA extensions in `.riscv.attributes` are not inspected by the header parser. */
|
||||
readonly riscv: ElfFamilyMetadata;
|
||||
readonly riscv: RiscvElfMetadata;
|
||||
readonly executableRole?: never;
|
||||
readonly managed?: never;
|
||||
})
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseRiscvAttributes,
|
||||
riscvAbiName,
|
||||
riscvFloatAbi,
|
||||
riscvIsaDeclaration,
|
||||
} from "./riscvIsaDeclaration.js";
|
||||
|
||||
const u32 = (value: number, little: boolean): number[] => {
|
||||
const bytes = Buffer.alloc(4);
|
||||
if (little) bytes.writeUInt32LE(value);
|
||||
else bytes.writeUInt32BE(value);
|
||||
return [...bytes];
|
||||
};
|
||||
const text = (value: string): number[] => [...Buffer.from(value, "latin1"), 0];
|
||||
|
||||
/** One vendor subsection holding one scoped list, as toolchains emit it. */
|
||||
const section = (
|
||||
attributes: readonly number[],
|
||||
{
|
||||
little = true,
|
||||
vendor = "riscv",
|
||||
scope = 1,
|
||||
}: {
|
||||
readonly little?: boolean;
|
||||
readonly vendor?: string;
|
||||
readonly scope?: number;
|
||||
} = {},
|
||||
): Buffer => {
|
||||
const scoped = [
|
||||
scope,
|
||||
...u32(1 + 4 + attributes.length, little),
|
||||
...attributes,
|
||||
];
|
||||
const name = text(vendor);
|
||||
return Buffer.from([
|
||||
0x41,
|
||||
...u32(4 + name.length + scoped.length, little),
|
||||
...name,
|
||||
...scoped,
|
||||
]);
|
||||
};
|
||||
|
||||
// Byte-for-byte the `.riscv.attributes` section LLVM 21 writes for
|
||||
// `-march=rv32imc -mabi=ilp32`: stack_align 16, then the ISA string.
|
||||
const LLVM_RV32IMC = Buffer.from(
|
||||
[
|
||||
"41350000",
|
||||
"00726973",
|
||||
"63760001",
|
||||
"2b000000",
|
||||
"04100572",
|
||||
"76333269",
|
||||
"3270315f",
|
||||
"6d327030",
|
||||
"5f633270",
|
||||
"305f7a6d",
|
||||
"6d756c31",
|
||||
"70305f7a",
|
||||
"63613170",
|
||||
"3000",
|
||||
].join(""),
|
||||
"hex",
|
||||
);
|
||||
|
||||
describe("RISC-V e_flags float ABI", () => {
|
||||
it.each([
|
||||
[0x0, "soft", "ilp32", "lp64"],
|
||||
[0x1, "soft", "ilp32", "lp64"],
|
||||
[0x2, "single", "ilp32f", "lp64f"],
|
||||
[0x3, "single", "ilp32f", "lp64f"],
|
||||
[0x4, "double", "ilp32d", "lp64d"],
|
||||
[0x5, "double", "ilp32d", "lp64d"],
|
||||
[0x6, "quad", null, "lp64q"],
|
||||
[0x1f, "quad", null, "lp64q"],
|
||||
[0x8000_0001, "soft", "ilp32", "lp64"],
|
||||
] as const)(
|
||||
"flags 0x%s declare the %s ABI",
|
||||
(flags, floatAbi, rv32, rv64) => {
|
||||
expect(riscvFloatAbi(flags)).toBe(floatAbi);
|
||||
expect(riscvAbiName(32, floatAbi)).toBe(rv32);
|
||||
expect(riscvAbiName(64, floatAbi)).toBe(rv64);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("RISC-V .riscv.attributes", () => {
|
||||
it("reads the ISA string LLVM writes", () => {
|
||||
expect(parseRiscvAttributes(LLVM_RV32IMC, true)).toEqual({
|
||||
ok: true,
|
||||
value: "rv32i2p1_m2p0_c2p0_zmmul1p0_zca1p0",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"reads lengths in the image's byte order (little=%s)",
|
||||
(little) => {
|
||||
expect(
|
||||
parseRiscvAttributes(
|
||||
section([5, ...text("rv32i2p1_c2p0")], { little }),
|
||||
little,
|
||||
),
|
||||
).toEqual({ ok: true, value: "rv32i2p1_c2p0" });
|
||||
},
|
||||
);
|
||||
|
||||
it("skips tags it does not use by the psABI's odd-string, even-integer rule", () => {
|
||||
expect(
|
||||
parseRiscvAttributes(
|
||||
section([
|
||||
4,
|
||||
16, // Tag_RISCV_stack_align
|
||||
6,
|
||||
1, // Tag_RISCV_unaligned_access
|
||||
8,
|
||||
0x80,
|
||||
0x01, // two-byte ULEB128
|
||||
5,
|
||||
...text("rv32i2p1"),
|
||||
7,
|
||||
...text("future string tag"),
|
||||
14,
|
||||
3, // Tag_RISCV_atomic_abi
|
||||
]),
|
||||
true,
|
||||
),
|
||||
).toEqual({ ok: true, value: "rv32i2p1" });
|
||||
});
|
||||
|
||||
it("reports a section without an ISA string as null", () => {
|
||||
expect(parseRiscvAttributes(section([4, 16]), true)).toEqual({
|
||||
ok: true,
|
||||
value: null,
|
||||
});
|
||||
expect(parseRiscvAttributes(Buffer.from([0x41]), true)).toEqual({
|
||||
ok: true,
|
||||
value: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["an empty section", Buffer.alloc(0), "format version"],
|
||||
["another format version", Buffer.from([0x42]), "format version"],
|
||||
[
|
||||
"a truncated subsection length",
|
||||
Buffer.from([0x41, 0x05, 0x00]),
|
||||
"truncated RISC-V attribute subsection length",
|
||||
],
|
||||
[
|
||||
"a subsection longer than the section",
|
||||
Buffer.from([0x41, 0xff, 0, 0, 0]),
|
||||
"does not fit the section",
|
||||
],
|
||||
[
|
||||
"another vendor",
|
||||
section([5, ...text("rv32i2p1")], { vendor: "gnu" }),
|
||||
"non-riscv vendor",
|
||||
],
|
||||
[
|
||||
"a section-scoped list",
|
||||
section([5, ...text("rv32i2p1")], { scope: 2 }),
|
||||
"scoped to sections or symbols",
|
||||
],
|
||||
[
|
||||
"a symbol-scoped list",
|
||||
section([5, ...text("rv32i2p1")], { scope: 3 }),
|
||||
"scoped to sections or symbols",
|
||||
],
|
||||
[
|
||||
"two ISA strings",
|
||||
section([5, ...text("rv32i2p1"), 5, ...text("rv32e2p0")]),
|
||||
"duplicate RISC-V Tag_RISCV_arch",
|
||||
],
|
||||
[
|
||||
"an unterminated string",
|
||||
section([5, ...Buffer.from("rv32i")]),
|
||||
"unterminated string value",
|
||||
],
|
||||
["an unterminated integer", section([4, 0x80]), "malformed integer value"],
|
||||
[
|
||||
"an integer beyond 32 bits",
|
||||
section([4, 0xff, 0xff, 0xff, 0xff, 0x7f]),
|
||||
"malformed integer value",
|
||||
],
|
||||
] as const)("refuses %s", (_label, bytes, reason) => {
|
||||
const parsed = parseRiscvAttributes(bytes, true);
|
||||
expect(parsed.ok).toBe(false);
|
||||
if (parsed.ok) return;
|
||||
expect(parsed.error).toContain(reason);
|
||||
});
|
||||
|
||||
it("refuses a second riscv subsection", () => {
|
||||
const one = section([5, ...text("rv32i2p1")]);
|
||||
expect(
|
||||
parseRiscvAttributes(Buffer.concat([one, one.subarray(1)]), true),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: "duplicate riscv vendor attribute subsections",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses a scope length that leaves its subsection", () => {
|
||||
const bytes = section([5, ...text("rv32i2p1")]);
|
||||
bytes.writeUInt32LE(0xff, 4 + 1 + 6 + 1);
|
||||
expect(parseRiscvAttributes(bytes, true)).toEqual({
|
||||
ok: false,
|
||||
error: "RISC-V attribute scope length does not fit its subsection",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("RISC-V Tag_RISCV_arch", () => {
|
||||
it.each([
|
||||
// LLVM 21 for the lane's soft-float fixture.
|
||||
[
|
||||
"rv32i2p1_m2p0_c2p0_zicsr2p0_zifencei2p0_zmmul1p0_zca1p0",
|
||||
32,
|
||||
"i",
|
||||
["m", "c", "zicsr", "zifencei", "zmmul", "zca"],
|
||||
],
|
||||
// GCC 14 for ESP-IDF's ESP32-C6 base string.
|
||||
[
|
||||
"rv32i2p1_m2p0_a2p1_c2p0_zicsr2p0_zifencei2p0_zaamo1p0_zalrsc1p0_zmmul1p0_zca1p0",
|
||||
32,
|
||||
"i",
|
||||
["m", "a", "c", "zicsr", "zifencei", "zaamo", "zalrsc", "zmmul", "zca"],
|
||||
],
|
||||
// ESP32-P4 with Espressif's vendor extensions and versions on them.
|
||||
[
|
||||
"rv32i2p1_m2p0_a2p1_f2p2_c2p0_zicsr2p0_xesploop1p0_xespv2p1",
|
||||
32,
|
||||
"i",
|
||||
["m", "a", "f", "c", "zicsr", "xesploop", "xespv"],
|
||||
],
|
||||
["rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0", 64, "i", ["m", "a", "f", "d", "c"]],
|
||||
["rv32e2p0_c2p0", 32, "e", ["c"]],
|
||||
["rv32i", 32, "i", []],
|
||||
// Digits inside a name are not a version.
|
||||
["rv64i2p1_zve32x1p0_zvl128b1p0", 64, "i", ["zve32x", "zvl128b"]],
|
||||
] as const)("reads %s", (arch, xlen, base, extensions) => {
|
||||
expect(riscvIsaDeclaration(arch)).toEqual({
|
||||
status: "declared",
|
||||
arch,
|
||||
xlen,
|
||||
base,
|
||||
extensions,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["", "canonical rv32/rv64"],
|
||||
["rv128i2p0", "canonical rv32/rv64"],
|
||||
["RV32I2P1", "canonical rv32/rv64"],
|
||||
["rv32imac", "canonical rv32/rv64"],
|
||||
["rv32i2p1__m2p0", 'non-canonical extension token ""'],
|
||||
["rv32i2p1_M2p0", 'non-canonical extension token "M2p0"'],
|
||||
["rv32i2p1_m2p0_m2p0", "declares extension m twice"],
|
||||
])("keeps %j unavailable with its raw value", (arch, reason) => {
|
||||
const declaration = riscvIsaDeclaration(arch);
|
||||
expect(declaration).toMatchObject({ status: "unavailable", arch });
|
||||
if (declaration.status !== "unavailable") return;
|
||||
expect(declaration.reason).toContain(reason);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
import type { RiscvIsaDeclaration } from "./binaryTargetTypes.js";
|
||||
import {
|
||||
readElfTableDeclarations,
|
||||
type ElfDeclarationRead,
|
||||
} from "./elfDeclaredRanges.js";
|
||||
import { readElfSectionHeader } from "./elfSectionHeader.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
|
||||
/** `EF_RISCV_FLOAT_ABI` declarations in `e_flags`, named as the psABI names them. */
|
||||
export const RISCV_FLOAT_ABIS = ["soft", "single", "double", "quad"] as const;
|
||||
export type RiscvFloatAbi = (typeof RISCV_FLOAT_ABIS)[number];
|
||||
|
||||
const EF_RISCV_FLOAT_ABI = 0x0006;
|
||||
|
||||
/** The float ABI that bits 1–2 of an ELF `e_flags` word declare. */
|
||||
export const riscvFloatAbi = (flags: number): RiscvFloatAbi =>
|
||||
RISCV_FLOAT_ABIS[(flags & EF_RISCV_FLOAT_ABI) >>> 1]!;
|
||||
|
||||
/** The psABI name of a class and float ABI; RV32 has no named quad-float ABI. */
|
||||
export const riscvAbiName = (
|
||||
elfClass: 32 | 64,
|
||||
floatAbi: RiscvFloatAbi,
|
||||
): string | null =>
|
||||
elfClass === 32 && floatAbi === "quad"
|
||||
? null
|
||||
: `${elfClass === 32 ? "ilp32" : "lp64"}${
|
||||
{ soft: "", single: "f", double: "d", quad: "q" }[floatAbi]
|
||||
}`;
|
||||
|
||||
/** `SHT_RISCV_ATTRIBUTES`, the psABI's build-attributes section type. */
|
||||
export const SHT_RISCV_ATTRIBUTES = 0x70000003;
|
||||
/** Bound the section read before its declared size can drive allocation. */
|
||||
export const RISCV_ATTRIBUTES_MAX_BYTES = 4096;
|
||||
|
||||
const FORMAT_VERSION = 0x41;
|
||||
const TAG_FILE = 1;
|
||||
const TAG_RISCV_ARCH = 5;
|
||||
|
||||
/**
|
||||
* Read `Tag_RISCV_arch` from a `.riscv.attributes` section; null means the
|
||||
* section declares no ISA string. The psABI gives odd tags string values and
|
||||
* even tags integer values, so tags REA does not use are skipped by that rule.
|
||||
* Other vendors and section or symbol scopes fail closed: their declarations
|
||||
* are not skipped.
|
||||
*/
|
||||
export const parseRiscvAttributes = (
|
||||
bytes: Uint8Array,
|
||||
littleEndian: boolean,
|
||||
): Result<string | null, string> => {
|
||||
if (bytes.length === 0 || bytes[0] !== FORMAT_VERSION)
|
||||
return err("unsupported RISC-V attribute section format version");
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const readU32 = (offset: number): number =>
|
||||
view.getUint32(offset, littleEndian);
|
||||
const readUleb = (
|
||||
offset: number,
|
||||
end: number,
|
||||
): { readonly value: number; readonly next: number } | undefined => {
|
||||
let value = 0;
|
||||
for (let index = 0; index < 5 && offset + index < end; index += 1) {
|
||||
const byte = bytes[offset + index]!;
|
||||
value += (byte & 0x7f) * 2 ** (7 * index);
|
||||
if ((byte & 0x80) === 0)
|
||||
return value <= 0xffffffff
|
||||
? { value, next: offset + index + 1 }
|
||||
: undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const readString = (
|
||||
offset: number,
|
||||
end: number,
|
||||
): { readonly value: string; readonly next: number } | undefined => {
|
||||
const stop = bytes.indexOf(0, offset);
|
||||
return stop < 0 || stop >= end
|
||||
? undefined
|
||||
: {
|
||||
value: Buffer.from(bytes.subarray(offset, stop)).toString("latin1"),
|
||||
next: stop + 1,
|
||||
};
|
||||
};
|
||||
|
||||
let arch: string | null = null;
|
||||
let vendorSeen = false;
|
||||
let position = 1;
|
||||
while (position < bytes.length) {
|
||||
if (bytes.length - position < 4)
|
||||
return err("truncated RISC-V attribute subsection length");
|
||||
const length = readU32(position);
|
||||
if (length < 4 || length > bytes.length - position)
|
||||
return err("RISC-V attribute subsection length does not fit the section");
|
||||
const end = position + length;
|
||||
const vendor = readString(position + 4, end);
|
||||
if (vendor === undefined)
|
||||
return err("unterminated RISC-V attribute vendor name");
|
||||
if (vendor.value !== "riscv")
|
||||
return err(
|
||||
"RISC-V ELF declares non-riscv vendor attributes that REA does not inspect",
|
||||
);
|
||||
if (vendorSeen) return err("duplicate riscv vendor attribute subsections");
|
||||
vendorSeen = true;
|
||||
|
||||
let cursor = vendor.next;
|
||||
while (cursor < end) {
|
||||
const scopeStart = cursor;
|
||||
const scope = readUleb(cursor, end);
|
||||
if (scope === undefined)
|
||||
return err("malformed RISC-V attribute scope tag");
|
||||
if (scope.value !== TAG_FILE)
|
||||
return err(
|
||||
"RISC-V attributes scoped to sections or symbols are not inspected by REA",
|
||||
);
|
||||
if (end - scope.next < 4)
|
||||
return err("truncated RISC-V attribute scope length");
|
||||
const scopeLength = readU32(scope.next);
|
||||
if (
|
||||
scopeLength < scope.next + 4 - scopeStart ||
|
||||
scopeLength > end - scopeStart
|
||||
)
|
||||
return err("RISC-V attribute scope length does not fit its subsection");
|
||||
const scopeEnd = scopeStart + scopeLength;
|
||||
cursor = scope.next + 4;
|
||||
while (cursor < scopeEnd) {
|
||||
const tag = readUleb(cursor, scopeEnd);
|
||||
if (tag === undefined) return err("malformed RISC-V attribute tag");
|
||||
if (tag.value % 2 === 1) {
|
||||
const value = readString(tag.next, scopeEnd);
|
||||
if (value === undefined)
|
||||
return err(
|
||||
`unterminated string value for RISC-V attribute tag ${tag.value}`,
|
||||
);
|
||||
if (tag.value === TAG_RISCV_ARCH) {
|
||||
if (arch !== null)
|
||||
return err("duplicate RISC-V Tag_RISCV_arch attributes");
|
||||
arch = value.value;
|
||||
}
|
||||
cursor = value.next;
|
||||
} else {
|
||||
const value = readUleb(tag.next, scopeEnd);
|
||||
if (value === undefined)
|
||||
return err(
|
||||
`malformed integer value for RISC-V attribute tag ${tag.value}`,
|
||||
);
|
||||
cursor = value.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
position = end;
|
||||
}
|
||||
return ok(arch);
|
||||
};
|
||||
|
||||
const ISA_BASE = /^rv(32|64)([ie])(?:\d+p\d+)?$/u;
|
||||
const ISA_EXTENSION = /^([a-z][a-z0-9]*?)(?:\d+p\d+)?$/u;
|
||||
|
||||
/**
|
||||
* Interpret a `Tag_RISCV_arch` string in the psABI's canonical form, such as
|
||||
* `rv32i2p1_m2p0_c2p0_zicsr2p0`: a base, then one underscore before each
|
||||
* extension, each with an optional `<major>p<minor>` version. A string in
|
||||
* another form keeps its raw value and stays unavailable.
|
||||
*/
|
||||
export const riscvIsaDeclaration = (arch: string): RiscvIsaDeclaration => {
|
||||
const unavailable = (reason: string): RiscvIsaDeclaration => ({
|
||||
status: "unavailable",
|
||||
reason,
|
||||
arch,
|
||||
});
|
||||
const [first = "", ...rest] = arch.split("_");
|
||||
const base = ISA_BASE.exec(first);
|
||||
if (base === null)
|
||||
return unavailable(
|
||||
"Tag_RISCV_arch does not begin with a canonical rv32/rv64 I or E base",
|
||||
);
|
||||
const extensions: string[] = [];
|
||||
for (const token of rest) {
|
||||
const name = ISA_EXTENSION.exec(token)?.[1];
|
||||
if (name === undefined)
|
||||
return unavailable(
|
||||
`Tag_RISCV_arch contains a non-canonical extension token "${token}"`,
|
||||
);
|
||||
if (extensions.includes(name))
|
||||
return unavailable(`Tag_RISCV_arch declares extension ${name} twice`);
|
||||
extensions.push(name);
|
||||
}
|
||||
return {
|
||||
status: "declared",
|
||||
arch,
|
||||
xlen: base[1] === "32" ? 32 : 64,
|
||||
base: base[2] === "e" ? "e" : "i",
|
||||
extensions,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Find and interpret the image's `SHT_RISCV_ATTRIBUTES` section. Every section
|
||||
* header is examined, so an absent declaration is observed, not assumed; two
|
||||
* attribute sections leave the declaration unavailable. The caller satisfies
|
||||
* each read from its own descriptor.
|
||||
*/
|
||||
export function* readRiscvIsaDeclaration(
|
||||
prefix: Buffer,
|
||||
fileSize: number,
|
||||
): Generator<ElfDeclarationRead, RiscvIsaDeclaration, Buffer> {
|
||||
const unavailable = (reason: string): RiscvIsaDeclaration => ({
|
||||
status: "unavailable",
|
||||
reason,
|
||||
});
|
||||
const declarations = yield* readElfTableDeclarations(prefix, fileSize);
|
||||
if (!declarations.ok) return unavailable(declarations.error);
|
||||
const { header, sections } = declarations.value;
|
||||
const little = header.byteOrder === "little";
|
||||
let attributes:
|
||||
| { readonly offset: bigint; readonly size: bigint }
|
||||
| undefined;
|
||||
const perRead = Math.max(1, Math.floor((64 * 1024) / sections.width));
|
||||
for (let index = 0; index < sections.count; index += perRead) {
|
||||
const count = Math.min(perRead, sections.count - index);
|
||||
const size = count * sections.width;
|
||||
const bytes = yield {
|
||||
offset: Number(sections.offset) + index * sections.width,
|
||||
size,
|
||||
};
|
||||
if (bytes.length !== size)
|
||||
return unavailable("truncated ELF section header table");
|
||||
for (let item = 0; item < count; item += 1) {
|
||||
const at = item * sections.width;
|
||||
const parsed = readElfSectionHeader(
|
||||
bytes.subarray(at, at + sections.width),
|
||||
header.elfClass,
|
||||
little,
|
||||
);
|
||||
if (!parsed.ok) return unavailable(parsed.error.message);
|
||||
if (parsed.value.type !== SHT_RISCV_ATTRIBUTES) continue;
|
||||
if (attributes !== undefined)
|
||||
return unavailable(
|
||||
"the image has more than one RISC-V attribute section",
|
||||
);
|
||||
attributes = { offset: parsed.value.offset, size: parsed.value.size };
|
||||
}
|
||||
}
|
||||
if (attributes === undefined) return { status: "absent" };
|
||||
if (
|
||||
attributes.size === 0n ||
|
||||
attributes.size > BigInt(RISCV_ATTRIBUTES_MAX_BYTES)
|
||||
)
|
||||
return unavailable(
|
||||
`RISC-V attribute section size ${String(attributes.size)} is outside REA's inspected bound of ${RISCV_ATTRIBUTES_MAX_BYTES} bytes`,
|
||||
);
|
||||
if (attributes.offset > BigInt(fileSize) - attributes.size)
|
||||
return unavailable("RISC-V attribute section lies outside the file");
|
||||
const size = Number(attributes.size);
|
||||
const bytes = yield { offset: Number(attributes.offset), size };
|
||||
if (bytes.length !== size)
|
||||
return unavailable("truncated RISC-V attribute section");
|
||||
const arch = parseRiscvAttributes(bytes, little);
|
||||
if (!arch.ok) return unavailable(arch.error);
|
||||
return arch.value === null
|
||||
? { status: "absent" }
|
||||
: riscvIsaDeclaration(arch.value);
|
||||
}
|
||||
@@ -6,11 +6,13 @@ const GHIDRA_ADMISSION_CODES = [
|
||||
"existing_project_raw_binary",
|
||||
"existing_project_language_override",
|
||||
"existing_project_mips_soft_float",
|
||||
"existing_project_riscv_calling_convention",
|
||||
"existing_project_analysis",
|
||||
"gamecube_dol_combined",
|
||||
"psp_combined",
|
||||
"psp_raw_binary",
|
||||
"mips_soft_float_scope",
|
||||
"riscv_calling_convention_scope",
|
||||
"language_override_dos",
|
||||
"language_override_raw_binary",
|
||||
"raw_binary_dos",
|
||||
|
||||
@@ -10,4 +10,5 @@ export const GHIDRA_BRIDGE_SOURCES = Object.freeze({
|
||||
analysisOptions: "ReaGhidraAnalysisOptions.java",
|
||||
noReturnFix: "ReaGhidraNoReturnFix.java",
|
||||
mipsSoftFloat: "ReaGhidraMipsSoftFloat.java",
|
||||
riscvCallingConvention: "ReaGhidraRiscvCallingConvention.java",
|
||||
} as const);
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type GhidraSeedCommitment,
|
||||
} from "./GhidraAnalysisSeeds.js";
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
import type { GhidraRiscvCallingConvention } from "./GhidraRiscvProfile.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import { mkdir, readFile, realpath } from "node:fs/promises";
|
||||
import { basename, dirname, join, posix, win32 } from "node:path";
|
||||
@@ -252,6 +253,8 @@ export interface GhidraHeadlessLauncherOptions {
|
||||
readonly pspExtension?: GhidraPspExtension;
|
||||
/** Remap typed float storage for the soft-float o32 ABI after analysis. */
|
||||
readonly mipsSoftFloat?: true;
|
||||
/** Apply this committed RISC-V calling-convention policy after analysis. */
|
||||
readonly riscvCallingConvention?: GhidraRiscvCallingConvention;
|
||||
/** Spawn seam for provider-boundary lifecycle tests. */
|
||||
readonly spawnProcess?: typeof spawnOwnedProviderProcess;
|
||||
/** LaunchSupport probe seam for Java-free launcher fixtures. */
|
||||
@@ -415,6 +418,11 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
...(this.options.mipsSoftFloat === undefined
|
||||
? {}
|
||||
: { mipsSoftFloat: this.options.mipsSoftFloat }),
|
||||
...(this.options.riscvCallingConvention === undefined
|
||||
? {}
|
||||
: {
|
||||
riscvCallingConvention: this.options.riscvCallingConvention,
|
||||
}),
|
||||
projectRoot: paths.projectRoot,
|
||||
targetPath: session.targetPath,
|
||||
...(existingProject === undefined ? {} : { existingProject }),
|
||||
@@ -767,6 +775,7 @@ export interface GhidraHeadlessArgumentOptions {
|
||||
readonly scriptLogPath: string;
|
||||
readonly psp?: true;
|
||||
readonly mipsSoftFloat?: true;
|
||||
readonly riscvCallingConvention?: GhidraRiscvCallingConvention;
|
||||
readonly dosMz?: true;
|
||||
readonly dosCom?: true;
|
||||
readonly rawBinary?: {
|
||||
@@ -796,6 +805,7 @@ type ExistingProjectOptions = Pick<
|
||||
| "rawBinary"
|
||||
| "languageOverride"
|
||||
| "mipsSoftFloat"
|
||||
| "riscvCallingConvention"
|
||||
| "seedPath"
|
||||
| "disabledAnalyzers"
|
||||
> & { readonly analysisSeeds?: GhidraSeedCommitment };
|
||||
@@ -824,6 +834,10 @@ const validateExistingProjectOptions = (
|
||||
throw new GhidraAdmissionError("existing_project_language_override");
|
||||
if (options.mipsSoftFloat === true)
|
||||
throw new GhidraAdmissionError("existing_project_mips_soft_float");
|
||||
if (options.riscvCallingConvention !== undefined)
|
||||
throw new GhidraAdmissionError(
|
||||
"existing_project_riscv_calling_convention",
|
||||
);
|
||||
if (
|
||||
options.seedPath !== undefined ||
|
||||
options.analysisSeeds !== undefined ||
|
||||
@@ -853,6 +867,7 @@ export const ghidraHeadlessArguments = (
|
||||
options.dosMz === true ||
|
||||
options.dosCom === true ||
|
||||
options.mipsSoftFloat === true ||
|
||||
options.riscvCallingConvention !== undefined ||
|
||||
rawBinary !== undefined ||
|
||||
override !== undefined)
|
||||
)
|
||||
@@ -874,6 +889,15 @@ export const ghidraHeadlessArguments = (
|
||||
rawBinary !== undefined)
|
||||
)
|
||||
throw new GhidraAdmissionError("mips_soft_float_scope");
|
||||
if (
|
||||
options.riscvCallingConvention !== undefined &&
|
||||
(options.psp === true ||
|
||||
options.mipsSoftFloat === true ||
|
||||
options.dosMz === true ||
|
||||
options.dosCom === true ||
|
||||
rawBinary !== undefined)
|
||||
)
|
||||
throw new GhidraAdmissionError("riscv_calling_convention_scope");
|
||||
if (
|
||||
override !== undefined &&
|
||||
(options.dosMz === true || options.dosCom === true)
|
||||
@@ -1024,6 +1048,16 @@ export const ghidraHeadlessArguments = (
|
||||
scriptPath.join(bridgeDirectory, GHIDRA_BRIDGE_SOURCES.mipsSoftFloat),
|
||||
]
|
||||
: []),
|
||||
...(options.riscvCallingConvention === undefined
|
||||
? []
|
||||
: [
|
||||
"-postScript",
|
||||
scriptPath.join(
|
||||
bridgeDirectory,
|
||||
GHIDRA_BRIDGE_SOURCES.riscvCallingConvention,
|
||||
),
|
||||
options.riscvCallingConvention,
|
||||
]),
|
||||
"-postScript",
|
||||
// Ghidra checks the caller's cwd before scriptPath for a basename. Select
|
||||
// the packaged source explicitly so unrelated entries cannot shadow it.
|
||||
|
||||
@@ -19,6 +19,9 @@ export const ghidraProjectHistoryEvidence = (
|
||||
...(info.mips_soft_float_storage === undefined
|
||||
? {}
|
||||
: { mips_soft_float_storage: info.mips_soft_float_storage }),
|
||||
...(info.riscv_calling_convention === undefined
|
||||
? {}
|
||||
: { riscv_calling_convention: info.riscv_calling_convention }),
|
||||
};
|
||||
if (Object.keys(reports).length === 0) return undefined;
|
||||
return createEvidence(
|
||||
|
||||
@@ -88,7 +88,12 @@ import {
|
||||
GHIDRA_PSP_LIMITATIONS,
|
||||
} from "./GhidraPspProfile.js";
|
||||
import { ghidraPspExtensionSchema } from "./GhidraPspExtension.js";
|
||||
import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js";
|
||||
import {
|
||||
ghidraRiscvCallingConvention,
|
||||
ghidraRiscvCallingConventionFailure,
|
||||
ghidraRiscvCallingConventionLimitations,
|
||||
ghidraRiscvLimitations,
|
||||
} from "./GhidraRiscvProfile.js";
|
||||
import { ghidraPePdbLimitations } from "./GhidraPeProfile.js";
|
||||
import { unverifiedGhidraBuildLimitation } from "./GhidraReleasePolicy.js";
|
||||
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
|
||||
@@ -195,6 +200,19 @@ export const createGhidraProviderClient = (input: {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const riscvCallingConvention = ghidraRiscvCallingConvention(target);
|
||||
if (
|
||||
riscvCallingConvention !==
|
||||
committedProfile.parameters.riscv_calling_convention
|
||||
)
|
||||
return err(
|
||||
new ProviderAdapterError("ghidra", "open_binary", {
|
||||
diagnostics: {
|
||||
reason:
|
||||
"Resolve the RISC-V calling-convention policy into the analysis profile before opening the session.",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const extensionProfile = ghidraExtensionSchema
|
||||
.array()
|
||||
.safeParse(committedProfile.parameters.analysis_extensions ?? []);
|
||||
@@ -349,6 +367,10 @@ export const createGhidraProviderClient = (input: {
|
||||
const softFloatLimitations: string[] = [
|
||||
...ghidraMipsSoftFloatLimitations(softFloat),
|
||||
];
|
||||
// Replaced by the reported prototype outcome once the handshake is checked.
|
||||
const riscvCallingConventionLimitations: string[] = [
|
||||
...ghidraRiscvCallingConventionLimitations(riscvCallingConvention),
|
||||
];
|
||||
// Replaced by the reported per-program outcome once the handshake is checked.
|
||||
const disabledAnalyzerLimitations: string[] = [];
|
||||
const rawLanguage =
|
||||
@@ -443,6 +465,9 @@ export const createGhidraProviderClient = (input: {
|
||||
? { pspExtension: pspExtension.data }
|
||||
: {}),
|
||||
...(softFloat ? { mipsSoftFloat: true as const } : {}),
|
||||
...(riscvCallingConvention === undefined
|
||||
? {}
|
||||
: { riscvCallingConvention }),
|
||||
...(target.format === "dos-mz" ? { dosMz: true } : {}),
|
||||
...(target.format === "dos-com" ? { dosCom: true } : {}),
|
||||
...(rawLanguage === undefined || target.format !== "raw-binary"
|
||||
@@ -542,10 +567,12 @@ export const createGhidraProviderClient = (input: {
|
||||
info,
|
||||
);
|
||||
} else {
|
||||
const softFloatReason = ghidraMipsSoftFloatFailure(
|
||||
softFloat,
|
||||
info.mips_soft_float_storage,
|
||||
);
|
||||
const callingConventionReason =
|
||||
ghidraMipsSoftFloatFailure(softFloat, info.mips_soft_float_storage) ??
|
||||
ghidraRiscvCallingConventionFailure(
|
||||
riscvCallingConvention,
|
||||
info.riscv_calling_convention,
|
||||
);
|
||||
extensionFailure =
|
||||
ghidraExtensionFailure(
|
||||
extensions,
|
||||
@@ -558,10 +585,10 @@ export const createGhidraProviderClient = (input: {
|
||||
info.disabled_analyzers,
|
||||
operation,
|
||||
) ??
|
||||
(softFloatReason === undefined
|
||||
(callingConventionReason === undefined
|
||||
? undefined
|
||||
: new ProviderAdapterError("ghidra", operation, {
|
||||
diagnostics: { reason: softFloatReason },
|
||||
diagnostics: { reason: callingConventionReason },
|
||||
}));
|
||||
if (extensionFailure === undefined && info.analysis_seeds !== undefined)
|
||||
seedLimitations.splice(
|
||||
@@ -578,6 +605,15 @@ export const createGhidraProviderClient = (input: {
|
||||
info.mips_soft_float_storage,
|
||||
),
|
||||
);
|
||||
if (extensionFailure === undefined)
|
||||
riscvCallingConventionLimitations.splice(
|
||||
0,
|
||||
riscvCallingConventionLimitations.length,
|
||||
...ghidraRiscvCallingConventionLimitations(
|
||||
riscvCallingConvention,
|
||||
info.riscv_calling_convention,
|
||||
),
|
||||
);
|
||||
if (extensionFailure === undefined)
|
||||
disabledAnalyzerLimitations.splice(
|
||||
0,
|
||||
@@ -621,6 +657,7 @@ export const createGhidraProviderClient = (input: {
|
||||
...targetLimitations,
|
||||
...(psp ? GHIDRA_PSP_LIMITATIONS : []),
|
||||
...softFloatLimitations,
|
||||
...riscvCallingConventionLimitations,
|
||||
...ghidraExtensionLimitations(extensions),
|
||||
...seedLimitations,
|
||||
...disabledAnalyzerLimitations,
|
||||
@@ -767,7 +804,8 @@ export const createGhidraProviderClient = (input: {
|
||||
extensions.length > 0 ||
|
||||
seeds !== undefined ||
|
||||
disabledAnalyzers !== undefined ||
|
||||
softFloat
|
||||
softFloat ||
|
||||
riscvCallingConvention !== undefined
|
||||
) {
|
||||
const started = await startSession(operation, options);
|
||||
if (!started.ok) return started;
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, parse, relative } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { parseConfig } from "../config/parseConfig.js";
|
||||
import type {
|
||||
BinaryTarget,
|
||||
ElfFamilyMetadata,
|
||||
RiscvElfMetadata,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
import { riscvIsaDeclaration } from "../domain/riscvIsaDeclaration.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import { HopperProvider } from "../hopper/HopperProvider.js";
|
||||
import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
|
||||
import { IdaProvider } from "../ida/IdaProvider.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import {
|
||||
GHIDRA_RISCV32_DECODED_EXTENSIONS,
|
||||
GHIDRA_RISCV_CALLING_CONVENTIONS,
|
||||
GHIDRA_RISCV_CALLING_CONVENTION_IDS,
|
||||
GHIDRA_RISCV_LANES,
|
||||
ghidraRiscvCallingConvention,
|
||||
ghidraRiscvCallingConventionFailure,
|
||||
ghidraRiscvLimitations,
|
||||
ghidraRiscvSupportLane,
|
||||
ghidraRiscvUnsupportedReason,
|
||||
type GhidraRiscvCallingConventionReport,
|
||||
} from "./GhidraRiscvProfile.js";
|
||||
import { ghidraProjectHistoryEvidence } from "./GhidraProjectHistory.js";
|
||||
import {
|
||||
GHIDRA_SESSION_CAPABILITIES,
|
||||
type GhidraSessionInfo,
|
||||
} from "./GhidraSessionValues.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import {
|
||||
ghidraPinnedLanguage,
|
||||
@@ -63,7 +78,7 @@ const configOf = (
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value;
|
||||
};
|
||||
const target = (change: Partial<ElfFamilyMetadata> = {}): BinaryTarget => ({
|
||||
const target = (change: Partial<RiscvElfMetadata> = {}): BinaryTarget => ({
|
||||
path: "/fixture.elf",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
@@ -78,6 +93,18 @@ const target = (change: Partial<ElfFamilyMetadata> = {}): BinaryTarget => ({
|
||||
...change,
|
||||
},
|
||||
});
|
||||
// ESP-IDF's ESP32-C3 base string, as LLVM records it for the lane's fixture.
|
||||
const C3_ARCH = "rv32i2p1_m2p0_c2p0_zicsr2p0_zifencei2p0_zmmul1p0";
|
||||
const ilp32 = (
|
||||
arch: string = C3_ARCH,
|
||||
change: Partial<RiscvElfMetadata> = {},
|
||||
): BinaryTarget =>
|
||||
target({
|
||||
elfClass: 32,
|
||||
flags: 0x1,
|
||||
isa: riscvIsaDeclaration(arch),
|
||||
...change,
|
||||
});
|
||||
|
||||
let installDir = "";
|
||||
beforeAll(async () => {
|
||||
@@ -205,12 +232,27 @@ const registerRiscvAdmissionAndCapabilityCases = () => {
|
||||
[{ type: 0xffa0 }, "e_type 65440"],
|
||||
[{ flags: 0xc }, "EF_RISCV_RVE"],
|
||||
[{ flags: 0x14 }, "EF_RISCV_TSO"],
|
||||
[{ flags: 0x1 }, "soft-float convention"],
|
||||
[{ flags: 0x3 }, "single-float"],
|
||||
[{ flags: 0x7 }, "quad-float"],
|
||||
[{ flags: 0x1 }, "soft-float ABI (lp64) declared by e_flags"],
|
||||
[{ flags: 0x3 }, "single-float ABI (lp64f)"],
|
||||
[{ flags: 0x7 }, "quad-float ABI (lp64q)"],
|
||||
[{ elfClass: 32, flags: 0x3 }, "single-float ABI (ilp32f)"],
|
||||
[{ elfClass: 32, flags: 0x7 }, "the quad-float ABI declared by e_flags"],
|
||||
[{ elfClass: 32, flags: 0x1 }, "which was not inspected"],
|
||||
[{ elfClass: 32, flags: 0x1, isa: { status: "absent" } }, "declares none"],
|
||||
[
|
||||
{
|
||||
elfClass: 32,
|
||||
flags: 0x1,
|
||||
isa: {
|
||||
status: "unavailable",
|
||||
reason: "truncated RISC-V attribute section",
|
||||
},
|
||||
},
|
||||
"readable Tag_RISCV_arch declaration: truncated RISC-V attribute section",
|
||||
],
|
||||
[{ flags: 0x8000_0005 }, "(0x80000000)"],
|
||||
[{ flags: 0x24 }, "bits outside the verified lane"],
|
||||
] satisfies [Partial<ElfFamilyMetadata>, string][])(
|
||||
] satisfies [Partial<RiscvElfMetadata>, string][])(
|
||||
"refuses an unverified RISC-V interpretation %j",
|
||||
async (change, reason) => {
|
||||
const provider = ghidra();
|
||||
@@ -272,9 +314,14 @@ const registerRiscvAdmissionAndCapabilityCases = () => {
|
||||
|
||||
it("states the linkage claim boundary in the lane limitations", () => {
|
||||
const notes = ghidraRiscvLimitations(target());
|
||||
expect(notes).toHaveLength(3);
|
||||
expect(notes.join(" ")).toContain("does not establish static linkage");
|
||||
expect(notes[0]).toContain("RISCV:LE:64:default");
|
||||
// Facts, independent of the owner's wording and order: the pinned
|
||||
// language and the static-linkage boundary.
|
||||
expect(notes).toContainEqual(
|
||||
expect.stringContaining("RISCV:LE:64:default"),
|
||||
);
|
||||
expect(notes).toContainEqual(
|
||||
expect.stringContaining("does not establish static linkage"),
|
||||
);
|
||||
expect(
|
||||
ghidraRiscvLimitations({
|
||||
path: "/other.elf",
|
||||
@@ -309,7 +356,350 @@ const registerRiscvAdmissionAndCapabilityCases = () => {
|
||||
});
|
||||
};
|
||||
|
||||
describe("RISC-V lane and calling-convention tables", () => {
|
||||
it("builds each lane id from its fields and keeps the double-float ids", () => {
|
||||
expect(GHIDRA_RISCV_LANES.map(ghidraRiscvSupportLane)).toEqual([
|
||||
"elf64-exec-le-double-float-generic-v1",
|
||||
"elf32-exec-le-double-float-generic-v1",
|
||||
"elf32-exec-le-soft-float-generic-v1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("gives every non-double lane one policy and every policy one lane", () => {
|
||||
const corrected = GHIDRA_RISCV_LANES.filter(
|
||||
(lane) => lane.floatAbi !== "double",
|
||||
);
|
||||
expect(corrected.map((lane) => lane.callingConvention)).toEqual(
|
||||
GHIDRA_RISCV_CALLING_CONVENTION_IDS,
|
||||
);
|
||||
expect(
|
||||
GHIDRA_RISCV_LANES.filter((lane) => lane.floatAbi === "double").every(
|
||||
(lane) => lane.callingConvention === undefined,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(GHIDRA_RISCV_CALLING_CONVENTIONS).toEqual({
|
||||
"ilp32-prototype-v1": { abi: "ilp32", prototype: "__rea_ilp32" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the decision record in docs/ghidra-riscv.md in step with the tables", async () => {
|
||||
const docs = await readFile(
|
||||
new URL("../../docs/ghidra-riscv.md", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
for (const lane of GHIDRA_RISCV_LANES)
|
||||
expect(docs).toContain(`\`${ghidraRiscvSupportLane(lane)}\``);
|
||||
for (const id of GHIDRA_RISCV_CALLING_CONVENTION_IDS) {
|
||||
const { abi, prototype } = GHIDRA_RISCV_CALLING_CONVENTIONS[id];
|
||||
expect(docs).toContain(`Policy \`${id}\`, prototype \`${prototype}\``);
|
||||
expect(docs).toContain(`### ${abi}\n`);
|
||||
}
|
||||
const decoded =
|
||||
/implements with\s+p-code: (.+?)\.\s+Any\s+other\s+extension/su.exec(
|
||||
docs,
|
||||
)?.[1];
|
||||
expect(
|
||||
new Set(
|
||||
[...(decoded ?? "").matchAll(/`([a-z0-9]+)`/gu)].map((m) => m[1]),
|
||||
),
|
||||
).toEqual(GHIDRA_RISCV32_DECODED_EXTENSIONS);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bounded Ghidra RISC-V admission", () => {
|
||||
registerRiscvLanguageCommitmentCases();
|
||||
registerRiscvAdmissionAndCapabilityCases();
|
||||
});
|
||||
|
||||
describe("RISC-V ilp32 admission", () => {
|
||||
it.each([
|
||||
["ESP32-C2/C3 base", C3_ARCH],
|
||||
[
|
||||
"ESP32-C5/C6/C61/H2/H21 base",
|
||||
"rv32i2p1_m2p0_a2p1_c2p0_zicsr2p0_zifencei2p0_zaamo1p0_zalrsc1p0_zmmul1p0_zca1p0",
|
||||
],
|
||||
[
|
||||
"soft-float ABI on F and D hardware",
|
||||
"rv32i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zmmul1p0_zca1p0_zcf1p0_zcd1p0",
|
||||
],
|
||||
["the bare base", "rv32i2p1"],
|
||||
])("admits the %s", async (_label, arch) => {
|
||||
const value = ilp32(arch);
|
||||
expect(ghidra().inspectTargetSupport(value).status).toBe("supported");
|
||||
expect(ghidraPinnedLanguage(value)).toEqual({
|
||||
languageId: "RISCV:LE:32:default",
|
||||
compilerSpecId: "gcc",
|
||||
});
|
||||
expect(ghidraRiscvCallingConvention(value)).toBe("ilp32-prototype-v1");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"ESP-IDF's optional bit-manipulation and Zc extensions",
|
||||
`${C3_ARCH}_zba1p0_zbb1p0_zbs1p0_zcb1p0_zcmp1p0_zcmt1p0`,
|
||||
"outside the set this lane decodes: zba, zbb, zbs, zcb, zcmp, zcmt.",
|
||||
],
|
||||
[
|
||||
"Espressif's vendor extensions",
|
||||
"rv32i2p1_m2p0_c2p0_xesploop1p0_xespv2p1_xespdsp1p0",
|
||||
"outside the set this lane decodes: xesploop, xespv, xespdsp.",
|
||||
],
|
||||
["an RV32E base", "rv32e2p0_m2p0_c2p0", "requires the RV32I base"],
|
||||
["an RV64 string", "rv64i2p1_m2p0_c2p0", "requires the RV32I base"],
|
||||
[
|
||||
"a non-canonical string",
|
||||
"rv32imac",
|
||||
"readable Tag_RISCV_arch declaration: Tag_RISCV_arch does not begin",
|
||||
],
|
||||
])("refuses %s", async (_label, arch, reason) => {
|
||||
const value = ilp32(arch);
|
||||
expect(ghidra().inspectTargetSupport(value)).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
reason: expect.stringContaining(reason),
|
||||
});
|
||||
expect((await ghidra().resolveAnalysisProfile(value)).ok).toBe(false);
|
||||
expect(ghidraPinnedLanguage(value)).toBeUndefined();
|
||||
expect(ghidraRiscvCallingConvention(value)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("commits the soft-float lane, its declarations and its policy", async () => {
|
||||
const resolved = await ghidra().resolveAnalysisProfile(ilp32());
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected an ilp32 profile");
|
||||
const parameters = resolved.value.profile.parameters;
|
||||
expect(parameters.riscv_elf).toEqual({
|
||||
elf_class: 32,
|
||||
byte_order: "little",
|
||||
type: 2,
|
||||
flags: 1,
|
||||
float_abi_double: false,
|
||||
float_abi: "soft",
|
||||
arch: C3_ARCH,
|
||||
rvc_declared: true,
|
||||
rve_declared: false,
|
||||
tso_declared: false,
|
||||
});
|
||||
expect(parameters.riscv_calling_convention).toBe("ilp32-prototype-v1");
|
||||
expect(parameters.riscv_support_lane).toBe(
|
||||
"elf32-exec-le-soft-float-generic-v1",
|
||||
);
|
||||
expect(parameters.language_id).toBe("RISCV:LE:32:default");
|
||||
expect(parameters.language_selection).toBe("provider-pinned-v1");
|
||||
const other = await ghidra().resolveAnalysisProfile(
|
||||
ilp32(`${C3_ARCH.replace("_zmmul1p0", "")}_zaamo1p0`),
|
||||
);
|
||||
if (!other.ok || other.value.profile === null)
|
||||
throw new Error("expected an ilp32 profile");
|
||||
expect(other.value.profile.digest).not.toBe(resolved.value.profile.digest);
|
||||
});
|
||||
|
||||
it("keeps double-float profiles exactly as before, whatever the ISA declaration", async () => {
|
||||
const plain = await ghidra().resolveAnalysisProfile(
|
||||
target({ elfClass: 32, flags: 0x5 }),
|
||||
);
|
||||
const declared = await ghidra().resolveAnalysisProfile(
|
||||
target({
|
||||
elfClass: 32,
|
||||
flags: 0x5,
|
||||
isa: riscvIsaDeclaration("rv32i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zcmp1p0"),
|
||||
}),
|
||||
);
|
||||
if (
|
||||
!plain.ok ||
|
||||
!declared.ok ||
|
||||
plain.value.profile === null ||
|
||||
declared.value.profile === null
|
||||
)
|
||||
throw new Error("expected double-float profiles");
|
||||
expect(plain.value.profile.parameters.riscv_elf).toEqual({
|
||||
elf_class: 32,
|
||||
byte_order: "little",
|
||||
type: 2,
|
||||
flags: 5,
|
||||
float_abi_double: true,
|
||||
rvc_declared: true,
|
||||
rve_declared: false,
|
||||
tso_declared: false,
|
||||
});
|
||||
expect(plain.value.profile.parameters).not.toHaveProperty(
|
||||
"riscv_calling_convention",
|
||||
);
|
||||
expect(declared.value.profile.digest).toBe(plain.value.profile.digest);
|
||||
});
|
||||
|
||||
it("states the declared ABI, the applied prototype and the declared ISA", () => {
|
||||
const notes = ghidraRiscvLimitations(ilp32());
|
||||
// One note names the declared ABI, the ABI the pinned language models
|
||||
// and the prototype REA applies, in any wording and order.
|
||||
expect(notes).toContainEqual(
|
||||
expect.stringMatching(
|
||||
/^(?=.*\(ilp32\))(?=.*\bilp32d\b)(?=.*`__rea_ilp32`)/su,
|
||||
),
|
||||
);
|
||||
expect(notes).toContainEqual(expect.stringContaining(C3_ARCH));
|
||||
});
|
||||
});
|
||||
|
||||
const report: GhidraRiscvCallingConventionReport = {
|
||||
policy: "ilp32-prototype-v1",
|
||||
functions: 11,
|
||||
definitions: 11,
|
||||
};
|
||||
const sessionInfo = (reported?: GhidraRiscvCallingConventionReport) => ({
|
||||
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: "RISCV:LE:32:default",
|
||||
compiler_spec_id: "gcc",
|
||||
image_base: "0x0",
|
||||
default_address_space: "ram",
|
||||
sha256: "a".repeat(64),
|
||||
},
|
||||
...(reported === undefined ? {} : { riscv_calling_convention: reported }),
|
||||
});
|
||||
const session = async (
|
||||
value: BinaryTarget,
|
||||
reported: GhidraRiscvCallingConventionReport | undefined,
|
||||
profileFor: BinaryTarget = value,
|
||||
) => {
|
||||
const counts = { starts: 0, calls: 0, closes: 0 };
|
||||
const launches: unknown[] = [];
|
||||
const provider = new GhidraProvider(
|
||||
configOf(installDir),
|
||||
silentLogger,
|
||||
{},
|
||||
host,
|
||||
(options) => {
|
||||
launches.push(options.launcher);
|
||||
return {
|
||||
start: () => {
|
||||
counts.starts++;
|
||||
return Promise.resolve(ok(sessionInfo(reported)));
|
||||
},
|
||||
callTool: () => {
|
||||
counts.calls++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
close: () => {
|
||||
counts.closes++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const resolved = await provider.resolveAnalysisProfile(profileFor);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a RISC-V profile");
|
||||
return {
|
||||
counts,
|
||||
launches,
|
||||
client: provider.createClient(value, resolved.value.profile),
|
||||
};
|
||||
};
|
||||
|
||||
describe("RISC-V calling-convention handshake", () => {
|
||||
it.each([
|
||||
["ilp32-prototype-v1", report, undefined],
|
||||
[undefined, undefined, undefined],
|
||||
[
|
||||
"ilp32-prototype-v1",
|
||||
undefined,
|
||||
"did not report the committed RISC-V calling-convention policy ilp32-prototype-v1",
|
||||
],
|
||||
[undefined, report, "profile did not commit"],
|
||||
] as const)(
|
||||
"expected %s with report %j fails with %s",
|
||||
(expected, reported, failure) => {
|
||||
const reason = ghidraRiscvCallingConventionFailure(expected, reported);
|
||||
if (failure === undefined) expect(reason).toBeUndefined();
|
||||
else expect(reason).toContain(failure);
|
||||
},
|
||||
);
|
||||
|
||||
it("launches the policy and reports its outcome as session limitations", async () => {
|
||||
const { client, launches } = await session(ilp32(), report);
|
||||
const health = await client.execute("health", {});
|
||||
if (!health.ok) throw health.error;
|
||||
expect(launches).toEqual([
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
riscvCallingConvention: "ilp32-prototype-v1",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(health.value.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
"installs a program-specific `__rea_ilp32` prototype",
|
||||
),
|
||||
expect.stringContaining(
|
||||
"11 functions and 11 function definitions use __rea_ilp32.",
|
||||
),
|
||||
expect.stringContaining("variadic call's arguments"),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("launches double-float sessions without a policy", async () => {
|
||||
const { client, launches } = await session(target(), undefined);
|
||||
expect((await client.execute("health", {})).ok).toBe(true);
|
||||
expect(launches).toEqual([
|
||||
expect.objectContaining({
|
||||
options: expect.not.objectContaining({
|
||||
riscvCallingConvention: expect.anything(),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
reason: "an ilp32 session without a report before any analysis call",
|
||||
value: ilp32(),
|
||||
reported: undefined,
|
||||
operation: "list_procedures",
|
||||
},
|
||||
{
|
||||
reason: "a report from a double-float session",
|
||||
value: target(),
|
||||
reported: report,
|
||||
operation: "health",
|
||||
},
|
||||
] as const)("rejects $reason", async ({ value, reported, operation }) => {
|
||||
const { counts, client } = await session(value, reported);
|
||||
const failed = await client.execute(operation, {});
|
||||
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 policy disagrees with the target", async () => {
|
||||
for (const [value, profileFor] of [
|
||||
[ilp32(), target({ elfClass: 32 })],
|
||||
[target({ elfClass: 32 }), ilp32()],
|
||||
] as const) {
|
||||
const { counts, client } = await session(value, report, profileFor);
|
||||
expect((await client.execute("health", {})).ok).toBe(false);
|
||||
expect(counts.starts).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a caller project's stored report as history", async () => {
|
||||
const resolved = await ghidra().resolveAnalysisProfile(ilp32());
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected an ilp32 profile");
|
||||
const info: GhidraSessionInfo = sessionInfo(report);
|
||||
expect(
|
||||
ghidraProjectHistoryEvidence(ilp32(), resolved.value.profile, info)
|
||||
?.normalized_result,
|
||||
).toEqual({ origin: "caller-project", riscv_calling_convention: report });
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,87 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type {
|
||||
BinaryTarget,
|
||||
RiscvElfMetadata,
|
||||
RiscvIsaDeclaration,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
riscvAbiName,
|
||||
riscvFloatAbi,
|
||||
type RiscvFloatAbi,
|
||||
} from "../domain/riscvIsaDeclaration.js";
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
|
||||
/** Calling-convention policy ids an analysis profile can commit. */
|
||||
export const GHIDRA_RISCV_CALLING_CONVENTION_IDS = [
|
||||
"ilp32-prototype-v1",
|
||||
] as const;
|
||||
export type GhidraRiscvCallingConvention =
|
||||
(typeof GHIDRA_RISCV_CALLING_CONVENTION_IDS)[number];
|
||||
|
||||
/**
|
||||
* Ghidra's generic RISC-V languages model the double-float ABI: the shipped
|
||||
* `riscv.ldefs` maps `RISCV:LE:{32,64}:default` to the `riscv{32,64}-fp.cspec`
|
||||
* compiler specification. Soft, single and quad ABI declarations therefore need
|
||||
* their own verification rather than inheriting this lane.
|
||||
* The ABI and prototype of each policy REA applies after analysis.
|
||||
* `ReaGhidraRiscvCallingConvention.java` derives each prototype from the
|
||||
* pinned language's default `riscv32-fp.cspec` model, which is ilp32d;
|
||||
* docs/ghidra-riscv.md states the rules of each ABI.
|
||||
*/
|
||||
export const GHIDRA_RISCV_CALLING_CONVENTIONS: Readonly<
|
||||
Record<
|
||||
GhidraRiscvCallingConvention,
|
||||
{ readonly abi: string; readonly prototype: string }
|
||||
>
|
||||
> = {
|
||||
"ilp32-prototype-v1": { abi: "ilp32", prototype: "__rea_ilp32" },
|
||||
};
|
||||
|
||||
/** One admitted ELF class and declared float ABI. */
|
||||
export interface GhidraRiscvLane {
|
||||
readonly elfClass: 32 | 64;
|
||||
readonly floatAbi: RiscvFloatAbi;
|
||||
/** Absent: Ghidra's stock double-float model applies unchanged. */
|
||||
readonly callingConvention?: GhidraRiscvCallingConvention;
|
||||
}
|
||||
|
||||
/** Every class and float ABI pair the Ghidra lane admits; others are refused. */
|
||||
export const GHIDRA_RISCV_LANES: readonly GhidraRiscvLane[] = [
|
||||
{ elfClass: 64, floatAbi: "double" },
|
||||
{ elfClass: 32, floatAbi: "double" },
|
||||
{ elfClass: 32, floatAbi: "soft", callingConvention: "ilp32-prototype-v1" },
|
||||
];
|
||||
|
||||
/** Lane ids are built from their fields, so a new ABI or load model adds a value. */
|
||||
export const ghidraRiscvSupportLane = (lane: GhidraRiscvLane): string =>
|
||||
`elf${lane.elfClass}-exec-le-${lane.floatAbi}-float-generic-v1`;
|
||||
|
||||
/**
|
||||
* Extensions Ghidra 12.1.4's generic RV32 language implements with p-code, by
|
||||
* the names `Tag_RISCV_arch` records. Lanes with a calling-convention policy
|
||||
* admit an image only when it declares an RV32I base and only these.
|
||||
*/
|
||||
export const GHIDRA_RISCV32_DECODED_EXTENSIONS: ReadonlySet<string> = new Set([
|
||||
"m",
|
||||
"a",
|
||||
"f",
|
||||
"d",
|
||||
"c",
|
||||
"zicsr",
|
||||
"zifencei",
|
||||
"zmmul",
|
||||
"zaamo",
|
||||
"zalrsc",
|
||||
"zca",
|
||||
"zcf",
|
||||
"zcd",
|
||||
]);
|
||||
|
||||
/** Outcome the post-analysis script records for the bridge handshake. */
|
||||
export interface GhidraRiscvCallingConventionReport {
|
||||
readonly policy: GhidraRiscvCallingConvention;
|
||||
readonly functions: number;
|
||||
readonly definitions: number;
|
||||
}
|
||||
|
||||
const EF_RISCV_RVC = 0x0001;
|
||||
const EF_RISCV_FLOAT_ABI = 0x0006;
|
||||
const EF_RISCV_FLOAT_ABI_DOUBLE = 0x0004;
|
||||
const EF_RISCV_RVE = 0x0008;
|
||||
const EF_RISCV_TSO = 0x0010;
|
||||
|
||||
@@ -24,13 +95,53 @@ const ET_DYN = 3;
|
||||
*/
|
||||
const EF_RISCV_DECLARED_BITS = EF_RISCV_RVC | EF_RISCV_FLOAT_ABI;
|
||||
|
||||
/** Header declarations outside the verified generic-language lane. */
|
||||
const riscvUnsupportedFloatAbi = (abi: number): string =>
|
||||
abi === 0
|
||||
? "unspecified (soft-float convention)"
|
||||
: abi === 0x0002
|
||||
? "single-float"
|
||||
: "quad-float";
|
||||
/** The lane a class and declared float ABI select, if the table admits one. */
|
||||
const ghidraRiscvLane = (
|
||||
metadata: RiscvElfMetadata,
|
||||
): GhidraRiscvLane | undefined => {
|
||||
const floatAbi = riscvFloatAbi(metadata.flags);
|
||||
return GHIDRA_RISCV_LANES.find(
|
||||
(lane) => lane.elfClass === metadata.elfClass && lane.floatAbi === floatAbi,
|
||||
);
|
||||
};
|
||||
|
||||
/** Name what a refused float ABI lacks, from the table admission reads. */
|
||||
const ghidraRiscvFloatAbiRefusal = (metadata: RiscvElfMetadata): string => {
|
||||
const floatAbi = riscvFloatAbi(metadata.flags);
|
||||
const name = riscvAbiName(metadata.elfClass, floatAbi);
|
||||
const admitted = GHIDRA_RISCV_LANES.filter(
|
||||
(lane) => lane.elfClass === metadata.elfClass,
|
||||
).map((lane) => `${lane.floatAbi}-float`);
|
||||
return `This Ghidra RISC-V lane admits ELF${metadata.elfClass} images that declare the ${admitted.join(
|
||||
" or ",
|
||||
)} ABI; the ${floatAbi}-float ABI${
|
||||
name === null ? "" : ` (${name})`
|
||||
} declared by e_flags has no verified calling-convention policy.`;
|
||||
};
|
||||
|
||||
/** Lanes with a calling-convention policy admit declared, decoded RV32I only. */
|
||||
const ghidraRiscvIsaRefusal = (
|
||||
isa: RiscvIsaDeclaration | undefined,
|
||||
): string | null => {
|
||||
if (isa === undefined)
|
||||
return "This RISC-V lane requires the image's .riscv.attributes ISA declaration, which was not inspected.";
|
||||
if (isa.status === "absent")
|
||||
return "This RISC-V lane requires a Tag_RISCV_arch declaration in .riscv.attributes; the image declares none, so its extensions are unknown.";
|
||||
if (isa.status === "unavailable")
|
||||
return `This RISC-V lane requires a readable Tag_RISCV_arch declaration: ${isa.reason}.`;
|
||||
if (isa.xlen !== 32 || isa.base !== "i")
|
||||
return `Tag_RISCV_arch declares ${isa.arch}; this lane requires the RV32I base.`;
|
||||
const outside = isa.extensions.filter(
|
||||
(extension) => !GHIDRA_RISCV32_DECODED_EXTENSIONS.has(extension),
|
||||
);
|
||||
return outside.length === 0
|
||||
? null
|
||||
: `Tag_RISCV_arch declares extensions outside the set this lane decodes: ${outside.join(
|
||||
", ",
|
||||
)}. Ghidra 12.1.4's generic RV32 language implements only ${[
|
||||
...GHIDRA_RISCV32_DECODED_EXTENSIONS,
|
||||
].join(", ")} with p-code; see docs/ghidra-riscv.md.`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep family recognition separate from the deliberately bounded Ghidra lane.
|
||||
@@ -65,17 +176,26 @@ export const ghidraRiscvUnsupportedReason = (
|
||||
? "shared and position-independent"
|
||||
: `e_type ${metadata.type}`
|
||||
} images need separate load verification. ET_EXEC alone does not establish static linkage, which this lane does not inspect.`;
|
||||
const abi = metadata.flags & EF_RISCV_FLOAT_ABI;
|
||||
if (abi !== EF_RISCV_FLOAT_ABI_DOUBLE)
|
||||
return `This Ghidra RISC-V lane requires the EF_RISCV_FLOAT_ABI_DOUBLE declaration (${riscvUnsupportedFloatAbi(
|
||||
abi,
|
||||
)} observed); the pinned generic language uses the double-float compiler specification.`;
|
||||
const lane = ghidraRiscvLane(metadata);
|
||||
if (lane === undefined) return ghidraRiscvFloatAbiRefusal(metadata);
|
||||
const unsupportedFlags = (metadata.flags & ~EF_RISCV_DECLARED_BITS) >>> 0;
|
||||
if (unsupportedFlags !== 0)
|
||||
return `RISC-V e_flags declares bits outside the verified lane (0x${unsupportedFlags.toString(16)}); reserved or newer declarations are not treated as compatible with this profile.`;
|
||||
return null;
|
||||
return lane.callingConvention === undefined
|
||||
? null
|
||||
: ghidraRiscvIsaRefusal(metadata.isa);
|
||||
};
|
||||
|
||||
/** The calling-convention policy an admitted target's lane applies, if any. */
|
||||
export const ghidraRiscvCallingConvention = (
|
||||
target: BinaryTarget,
|
||||
): GhidraRiscvCallingConvention | undefined =>
|
||||
target.kind === "executable" &&
|
||||
target.architecture === "riscv" &&
|
||||
ghidraRiscvUnsupportedReason(target) === null
|
||||
? ghidraRiscvLane(target.riscv)?.callingConvention
|
||||
: undefined;
|
||||
|
||||
/** Pin a language only for declarations admitted by the same lane boundary. */
|
||||
export const ghidraRiscvLanguageOverride = (
|
||||
target: BinaryTarget,
|
||||
@@ -89,41 +209,99 @@ export const ghidraRiscvLanguageOverride = (
|
||||
}
|
||||
: undefined;
|
||||
|
||||
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
|
||||
/**
|
||||
* Commit source ELF interpretation without confusing target ISA with host CPU.
|
||||
* Lanes with a calling-convention policy also commit the declarations that
|
||||
* admitted them; double-float profiles keep their original parameters.
|
||||
*/
|
||||
export const ghidraRiscvProfileParameters = (
|
||||
target: BinaryTarget,
|
||||
): Readonly<Record<string, JsonValue>> =>
|
||||
target.kind === "executable" &&
|
||||
target.architecture === "riscv" &&
|
||||
target.riscv !== undefined
|
||||
? {
|
||||
riscv_elf: {
|
||||
elf_class: target.riscv.elfClass,
|
||||
byte_order: target.riscv.byteOrder,
|
||||
type: target.riscv.type,
|
||||
flags: target.riscv.flags,
|
||||
float_abi_double:
|
||||
(target.riscv.flags & EF_RISCV_FLOAT_ABI) ===
|
||||
EF_RISCV_FLOAT_ABI_DOUBLE,
|
||||
rvc_declared: (target.riscv.flags & EF_RISCV_RVC) !== 0,
|
||||
rve_declared: (target.riscv.flags & EF_RISCV_RVE) !== 0,
|
||||
tso_declared: (target.riscv.flags & EF_RISCV_TSO) !== 0,
|
||||
},
|
||||
riscv_support_lane:
|
||||
target.riscv.elfClass === 32
|
||||
? "elf32-exec-le-double-float-generic-v1"
|
||||
: "elf64-exec-le-double-float-generic-v1",
|
||||
}
|
||||
: {};
|
||||
): Readonly<Record<string, JsonValue>> => {
|
||||
if (
|
||||
target.kind !== "executable" ||
|
||||
target.architecture !== "riscv" ||
|
||||
target.riscv === undefined
|
||||
)
|
||||
return {};
|
||||
const floatAbi = riscvFloatAbi(target.riscv.flags);
|
||||
const convention = ghidraRiscvCallingConvention(target);
|
||||
const isa = target.riscv.isa;
|
||||
return {
|
||||
riscv_elf: {
|
||||
elf_class: target.riscv.elfClass,
|
||||
byte_order: target.riscv.byteOrder,
|
||||
type: target.riscv.type,
|
||||
flags: target.riscv.flags,
|
||||
float_abi_double: floatAbi === "double",
|
||||
rvc_declared: (target.riscv.flags & EF_RISCV_RVC) !== 0,
|
||||
rve_declared: (target.riscv.flags & EF_RISCV_RVE) !== 0,
|
||||
tso_declared: (target.riscv.flags & EF_RISCV_TSO) !== 0,
|
||||
...(convention === undefined || isa?.status !== "declared"
|
||||
? {}
|
||||
: { float_abi: floatAbi, arch: isa.arch }),
|
||||
},
|
||||
...(convention === undefined
|
||||
? {}
|
||||
: { riscv_calling_convention: convention }),
|
||||
riscv_support_lane: ghidraRiscvSupportLane({
|
||||
elfClass: target.riscv.elfClass,
|
||||
floatAbi,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/** The pinned language is an analysis convention, not proof of the producer. */
|
||||
export const ghidraRiscvLimitations = (
|
||||
target: BinaryTarget,
|
||||
): readonly string[] =>
|
||||
target.kind === "executable" && target.architecture === "riscv"
|
||||
? [
|
||||
`RISC-V ELF: REA pins Ghidra's generic \`RISCV:LE:${target.riscv.elfClass}:default\` language with the \`gcc\` compiler specification instead of accepting the loader's header-based choice, which selects \`RISCV:LE:32:AndeStar_v5\` for ordinary RV32 images on Ghidra 12.1.4.`,
|
||||
"The pinned generic language models the double-float ABI declared by `e_flags`; the declaration is recorded as an ELF fact and is not evidence of the original compiler or of verified instruction semantics.",
|
||||
"Admission reads `e_type` only: a fixed-address ET_EXEC declaration does not establish static linkage, and program headers (`PT_INTERP`, dynamic dependencies) are not inspected by this lane.",
|
||||
]
|
||||
: [];
|
||||
): readonly string[] => {
|
||||
if (target.kind !== "executable" || target.architecture !== "riscv")
|
||||
return [];
|
||||
const convention = ghidraRiscvCallingConvention(target);
|
||||
const floatAbi = riscvFloatAbi(target.riscv.flags);
|
||||
const isa = target.riscv.isa;
|
||||
return [
|
||||
`RISC-V ELF: REA pins Ghidra's generic \`RISCV:LE:${target.riscv.elfClass}:default\` language with the \`gcc\` compiler specification instead of accepting the loader's header-based choice, which selects \`RISCV:LE:32:AndeStar_v5\` for ordinary RV32 images on Ghidra 12.1.4.`,
|
||||
convention === undefined
|
||||
? "The pinned generic language models the double-float ABI declared by `e_flags`; the declaration is recorded as an ELF fact and is not evidence of the original compiler or of verified instruction semantics."
|
||||
: `The ELF header declares the ${floatAbi}-float ABI (${
|
||||
GHIDRA_RISCV_CALLING_CONVENTIONS[convention].abi
|
||||
}); the pinned language's compiler specification models ilp32d, so REA applies its \`${
|
||||
GHIDRA_RISCV_CALLING_CONVENTIONS[convention].prototype
|
||||
}\` prototype instead. The declaration is recorded as an ELF fact and is not evidence of the original compiler.`,
|
||||
...(convention !== undefined && isa?.status === "declared"
|
||||
? [
|
||||
`\`.riscv.attributes\` declares \`${isa.arch}\`. Every declared extension is in the set this lane decodes; the declaration does not prove that every instruction belongs to it.`,
|
||||
]
|
||||
: []),
|
||||
"Admission reads `e_type` only: a fixed-address ET_EXEC declaration does not establish static linkage, and program headers (`PT_INTERP`, dynamic dependencies) are not inspected by this lane.",
|
||||
];
|
||||
};
|
||||
|
||||
/** Fail closed unless the bridge applied exactly the committed policy. */
|
||||
export const ghidraRiscvCallingConventionFailure = (
|
||||
expected: GhidraRiscvCallingConvention | undefined,
|
||||
report: GhidraRiscvCallingConventionReport | undefined,
|
||||
): string | undefined =>
|
||||
expected === report?.policy
|
||||
? undefined
|
||||
: expected === undefined
|
||||
? "The Ghidra session reported a RISC-V calling-convention policy that the profile did not commit."
|
||||
: `The Ghidra session did not report the committed RISC-V calling-convention policy ${expected}.`;
|
||||
|
||||
/** State the prototype REA applies, and its outcome once the bridge reports it. */
|
||||
export const ghidraRiscvCallingConventionLimitations = (
|
||||
expected: GhidraRiscvCallingConvention | undefined,
|
||||
report?: GhidraRiscvCallingConventionReport,
|
||||
): readonly string[] => {
|
||||
if (expected === undefined) return [];
|
||||
const { abi, prototype } = GHIDRA_RISCV_CALLING_CONVENTIONS[expected];
|
||||
return [
|
||||
`RISC-V ${abi}: Ghidra's generic RV32 compiler specification models ilp32d. After analysis REA installs a program-specific \`${prototype}\` prototype, derived from the default prototype by policy ${expected} (docs/ghidra-riscv.md), and assigns it with dynamic storage to every function, external function and function definition. Thunks follow their targets.`,
|
||||
...(report === undefined
|
||||
? []
|
||||
: [
|
||||
`RISC-V calling-convention outcome: ${report.functions} functions and ${report.definitions} function definitions use ${prototype}.`,
|
||||
]),
|
||||
"Ghidra recovers a variadic call's arguments from the registers the caller writes, not from their types: a padding register can appear as an argument, and arguments after an unwritten register are omitted.",
|
||||
];
|
||||
};
|
||||
@@ -142,6 +142,38 @@ describe("Ghidra MIPS soft-float storage report", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra RISC-V calling-convention report", () => {
|
||||
const valid = {
|
||||
policy: "ilp32-prototype-v1",
|
||||
functions: 11,
|
||||
definitions: 11,
|
||||
};
|
||||
it("accepts the exact committed policy outcome", () => {
|
||||
const parsed = parseGhidraSessionInfo(
|
||||
{ ...session(), riscv_calling_convention: valid },
|
||||
expected,
|
||||
);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(parsed.value.riscv_calling_convention).toEqual(valid);
|
||||
});
|
||||
it.each([
|
||||
["an unknown policy", { ...valid, policy: "ilp32f-prototype-v1" }],
|
||||
["the MIPS policy", { ...valid, policy: "o32-soft-float-prototype-v1" }],
|
||||
["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, prototype: "__rea_ilp32" }],
|
||||
["a non-object report", "ilp32-prototype-v1"],
|
||||
])("rejects %s", (_label, report) => {
|
||||
expect(
|
||||
parseGhidraSessionInfo(
|
||||
{ ...session(), riscv_calling_convention: report },
|
||||
expected,
|
||||
).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra disabled analyzer report", () => {
|
||||
const valid = {
|
||||
format: "rea-ghidra-disabled-analyzers-v1",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
|
||||
import { ghidraExtensionResultSchema } from "./extensions/GhidraExtensions.js";
|
||||
import { GHIDRA_MIPS_SOFT_FLOAT_STORAGE } from "./GhidraMipsProfile.js";
|
||||
import { GHIDRA_RISCV_CALLING_CONVENTION_IDS } from "./GhidraRiscvProfile.js";
|
||||
|
||||
/** Exact methods proved by the selected Program bridge handshake. */
|
||||
export const GHIDRA_SESSION_CAPABILITIES = [
|
||||
@@ -95,6 +96,14 @@ const sessionInfoSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
riscv_calling_convention: z
|
||||
.object({
|
||||
policy: z.enum(GHIDRA_RISCV_CALLING_CONVENTION_IDS),
|
||||
functions: z.int().nonnegative(),
|
||||
definitions: z.int().nonnegative(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
capabilities: z.array(capabilitySchema),
|
||||
target: z
|
||||
.object({
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import type { RiscvIsaDeclaration } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import { ghidraRiscvUnsupportedReason } from "../../../src/ghidra/GhidraRiscvProfile.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const SHT_RISCV_ATTRIBUTES = 0x70000003;
|
||||
const SECTION_TABLE = 128;
|
||||
// Deliberately outside BinaryTargetResolver's 4 KiB probe.
|
||||
const PAYLOADS = 8192;
|
||||
|
||||
const attributes = (arch: string | null, vendor = "riscv"): Buffer => {
|
||||
const list = [
|
||||
4,
|
||||
16, // Tag_RISCV_stack_align
|
||||
...(arch === null ? [] : [5, ...Buffer.from(`${arch}\0`, "latin1")]),
|
||||
];
|
||||
const scoped = Buffer.alloc(5 + list.length);
|
||||
scoped[0] = 1; // Tag_File
|
||||
scoped.writeUInt32LE(scoped.length, 1);
|
||||
scoped.set(list, 5);
|
||||
const name = Buffer.from(`${vendor}\0`, "latin1");
|
||||
const header = Buffer.alloc(5);
|
||||
header[0] = 0x41;
|
||||
header.writeUInt32LE(4 + name.length + scoped.length, 1);
|
||||
return Buffer.concat([header, name, scoped]);
|
||||
};
|
||||
|
||||
/** An ELF32 RISC-V soft-float image with one section header per payload. */
|
||||
const elf = (
|
||||
sections: readonly { type: number; payload: Buffer }[],
|
||||
): Buffer => {
|
||||
const payloadBytes = sections.reduce(
|
||||
(total, section) => total + section.payload.length,
|
||||
0,
|
||||
);
|
||||
const bytes = Buffer.alloc(PAYLOADS + payloadBytes);
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1]);
|
||||
bytes.writeUInt16LE(2, 16); // ET_EXEC
|
||||
bytes.writeUInt16LE(243, 18); // EM_RISCV
|
||||
bytes.writeUInt32LE(1, 20);
|
||||
bytes.writeUInt32LE(SECTION_TABLE, 32);
|
||||
bytes.writeUInt32LE(0x1, 36); // RVC, soft-float ABI
|
||||
bytes.writeUInt16LE(52, 40);
|
||||
bytes.writeUInt16LE(32, 42);
|
||||
bytes.writeUInt16LE(40, 46);
|
||||
bytes.writeUInt16LE(1 + sections.length, 48);
|
||||
let offset = PAYLOADS;
|
||||
sections.forEach(({ type, payload }, index) => {
|
||||
const header = SECTION_TABLE + (1 + index) * 40;
|
||||
bytes.writeUInt32LE(type, header + 4);
|
||||
bytes.writeUInt32LE(offset, header + 16);
|
||||
bytes.writeUInt32LE(payload.length, header + 20);
|
||||
payload.copy(bytes, offset);
|
||||
offset += payload.length;
|
||||
});
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const resolveIsa = async (bytes: Buffer) => {
|
||||
const directory = await createTestTempDirectory("rea-riscv-isa-test-");
|
||||
const path = join(directory, "fixture.elf");
|
||||
await writeFile(path, bytes);
|
||||
const resolved = await parseBinaryTarget(path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
if (
|
||||
resolved.value.kind !== "executable" ||
|
||||
resolved.value.architecture !== "riscv"
|
||||
)
|
||||
throw new Error("Expected a resolved RISC-V executable");
|
||||
return {
|
||||
isa: resolved.value.riscv.isa,
|
||||
refusal: ghidraRiscvUnsupportedReason(resolved.value),
|
||||
};
|
||||
};
|
||||
|
||||
const C3_ARCH = "rv32i2p1_m2p0_c2p0_zicsr2p0_zifencei2p0_zmmul1p0";
|
||||
|
||||
describe("RISC-V ISA declarations at target resolution", () => {
|
||||
it("reads Tag_RISCV_arch beyond the header probe and admits the ilp32 lane", async () => {
|
||||
expect(
|
||||
await resolveIsa(
|
||||
elf([
|
||||
{ type: 1, payload: Buffer.from([0x13, 0, 0, 0]) },
|
||||
{ type: SHT_RISCV_ATTRIBUTES, payload: attributes(C3_ARCH) },
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
isa: {
|
||||
status: "declared",
|
||||
arch: C3_ARCH,
|
||||
xlen: 32,
|
||||
base: "i",
|
||||
extensions: ["m", "c", "zicsr", "zifencei", "zmmul"],
|
||||
},
|
||||
refusal: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["no attribute section", [], { status: "absent" }, "declares none"],
|
||||
[
|
||||
"an attribute section without Tag_RISCV_arch",
|
||||
[{ type: SHT_RISCV_ATTRIBUTES, payload: attributes(null) }],
|
||||
{ status: "absent" },
|
||||
"declares none",
|
||||
],
|
||||
[
|
||||
"another vendor's attributes",
|
||||
[{ type: SHT_RISCV_ATTRIBUTES, payload: attributes(C3_ARCH, "gnu") }],
|
||||
{
|
||||
status: "unavailable",
|
||||
reason:
|
||||
"RISC-V ELF declares non-riscv vendor attributes that REA does not inspect",
|
||||
},
|
||||
"readable Tag_RISCV_arch declaration",
|
||||
],
|
||||
[
|
||||
"two attribute sections",
|
||||
[
|
||||
{ type: SHT_RISCV_ATTRIBUTES, payload: attributes(C3_ARCH) },
|
||||
{ type: SHT_RISCV_ATTRIBUTES, payload: attributes(C3_ARCH) },
|
||||
],
|
||||
{
|
||||
status: "unavailable",
|
||||
reason: "the image has more than one RISC-V attribute section",
|
||||
},
|
||||
"more than one RISC-V attribute section",
|
||||
],
|
||||
[
|
||||
"an attribute section beyond the inspected bound",
|
||||
[{ type: SHT_RISCV_ATTRIBUTES, payload: Buffer.alloc(4097, 0x41) }],
|
||||
{
|
||||
status: "unavailable",
|
||||
reason:
|
||||
"RISC-V attribute section size 4097 is outside REA's inspected bound of 4096 bytes",
|
||||
},
|
||||
"inspected bound",
|
||||
],
|
||||
[
|
||||
"an extension outside the decoded set",
|
||||
[
|
||||
{
|
||||
type: SHT_RISCV_ATTRIBUTES,
|
||||
payload: attributes(`${C3_ARCH}_zcmp1p0`),
|
||||
},
|
||||
],
|
||||
{
|
||||
status: "declared",
|
||||
arch: `${C3_ARCH}_zcmp1p0`,
|
||||
xlen: 32,
|
||||
base: "i",
|
||||
extensions: ["m", "c", "zicsr", "zifencei", "zmmul", "zcmp"],
|
||||
},
|
||||
"outside the set this lane decodes: zcmp.",
|
||||
],
|
||||
] as const)(
|
||||
"keeps %s visible and refuses the ilp32 lane",
|
||||
async (_label, sections, isa: RiscvIsaDeclaration, refusal) => {
|
||||
const resolved = await resolveIsa(elf(sections));
|
||||
expect(resolved.isa).toEqual(isa);
|
||||
expect(resolved.refusal).toContain(refusal);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -236,6 +236,10 @@ describe("Ghidra GameCube DOL loader", () => {
|
||||
["DOS MZ", { dosMz: true }],
|
||||
["DOS COM", { dosCom: true }],
|
||||
["MIPS soft-float", { mipsSoftFloat: true }],
|
||||
[
|
||||
"RISC-V calling convention",
|
||||
{ riscvCallingConvention: "ilp32-prototype-v1" },
|
||||
],
|
||||
[
|
||||
"raw binary",
|
||||
{
|
||||
@@ -358,6 +362,67 @@ describe("Ghidra headless launcher", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the committed RISC-V policy after the no-return refit 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",
|
||||
languageOverride: {
|
||||
languageId: "RISCV:LE:32:default",
|
||||
compilerSpecId: "gcc",
|
||||
},
|
||||
};
|
||||
const script = "/package/bridge/ReaGhidraRiscvCallingConvention.java";
|
||||
// PSP, DOS and raw imports carry their own languages, so they omit the pin.
|
||||
const { languageOverride: _pin, ...unpinned } = base;
|
||||
expect(ghidraHeadlessArguments(base)).not.toContain(script);
|
||||
const arguments_ = ghidraHeadlessArguments({
|
||||
...base,
|
||||
riscvCallingConvention: "ilp32-prototype-v1",
|
||||
});
|
||||
const at = arguments_.indexOf(script);
|
||||
expect(arguments_.slice(at - 1, at + 2)).toEqual([
|
||||
"-postScript",
|
||||
script,
|
||||
"ilp32-prototype-v1",
|
||||
]);
|
||||
expect(
|
||||
arguments_.indexOf("/package/bridge/ReaGhidraNoReturnFix.java"),
|
||||
).toBeLessThan(at);
|
||||
expect(at).toBeLessThan(
|
||||
arguments_.indexOf("/package/bridge/ReaGhidraBridge.java"),
|
||||
);
|
||||
for (const conflict of [
|
||||
{ psp: true as const },
|
||||
{ mipsSoftFloat: true as const },
|
||||
{ dosMz: true as const },
|
||||
{ dosCom: true as const },
|
||||
{
|
||||
rawBinary: {
|
||||
languageId: "x86:LE:32:default",
|
||||
compilerSpecId: "default",
|
||||
baseAddr: "0x1000",
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(() =>
|
||||
ghidraHeadlessArguments({
|
||||
...unpinned,
|
||||
riscvCallingConvention: "ilp32-prototype-v1",
|
||||
...conflict,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
_tag: "GhidraAdmissionError",
|
||||
code: "riscv_calling_convention_scope",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra configured import language and seeds", () => {
|
||||
@@ -617,6 +682,10 @@ describe("Ghidra existing project", () => {
|
||||
],
|
||||
[{ psp: true as const }, "existing_project_psp"],
|
||||
[{ mipsSoftFloat: true as const }, "existing_project_mips_soft_float"],
|
||||
[
|
||||
{ riscvCallingConvention: "ilp32-prototype-v1" as const },
|
||||
"existing_project_riscv_calling_convention",
|
||||
],
|
||||
[{ seedPath: "/tmp/runtime/seeds/seeds.tsv" }, "existing_project_analysis"],
|
||||
[
|
||||
{ disabledAnalyzers: ["Decompiler Parameter ID"] },
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/* Source-owned RISC-V calling-convention fixture. Never execute this target.
|
||||
* One source for every float ABI the lane verifies: float and double
|
||||
* arguments and returns, a double that starts in an odd register, a double
|
||||
* split between a7 and the stack, small structures of two floats and of a
|
||||
* float and an int, a large structure and a long double passed and returned
|
||||
* by reference, a variadic call with aligned double pairs, and an indirect
|
||||
* call through a typed pointer. It moves floating-point values without arithmetic or
|
||||
* comparisons, so a soft-float build links no compiler-rt helpers. The
|
||||
* expected storage per ABI is in tests/conformance/ghidra/riscv-checks.mjs.
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
|
||||
volatile unsigned int rea_riscv_global = 7;
|
||||
volatile unsigned int rea_riscv_abi_sink;
|
||||
extern unsigned int rea_riscv_probe(unsigned int value);
|
||||
extern unsigned int rea_riscv_isa_probe(unsigned int value);
|
||||
|
||||
struct rea_riscv_abi_pair {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct rea_riscv_abi_tagged {
|
||||
float value;
|
||||
int tag;
|
||||
};
|
||||
|
||||
struct rea_riscv_abi_large {
|
||||
double first;
|
||||
double second;
|
||||
float third;
|
||||
};
|
||||
|
||||
__attribute__((noinline)) float rea_riscv_abi_pick(float a, float b, int sel)
|
||||
{
|
||||
return sel ? a : b;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) double rea_riscv_abi_wide(double a, int sel)
|
||||
{
|
||||
return sel ? a : 0.5;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) double rea_riscv_abi_mixed(int i, double d, float f, double e)
|
||||
{
|
||||
rea_riscv_abi_sink = *(volatile unsigned int *)&f;
|
||||
return i ? d : e;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) double rea_riscv_abi_split(int a, int b, int c, int d, int e, int f, int g, double x)
|
||||
{
|
||||
rea_riscv_abi_sink = (unsigned int)(a ^ b ^ c ^ d ^ e ^ f ^ g);
|
||||
return x;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) struct rea_riscv_abi_pair rea_riscv_abi_pair(struct rea_riscv_abi_pair value, float scale)
|
||||
{
|
||||
struct rea_riscv_abi_pair swapped = { value.y, value.x };
|
||||
rea_riscv_abi_sink = *(volatile unsigned int *)&scale;
|
||||
return swapped;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) struct rea_riscv_abi_tagged rea_riscv_abi_tagged(struct rea_riscv_abi_tagged value, int tag)
|
||||
{
|
||||
value.tag = tag;
|
||||
return value;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) struct rea_riscv_abi_large rea_riscv_abi_large(struct rea_riscv_abi_large value, float scale)
|
||||
{
|
||||
value.third = scale;
|
||||
return value;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) long double rea_riscv_abi_quad(long double value, int sel)
|
||||
{
|
||||
rea_riscv_abi_sink = (unsigned int)sel;
|
||||
return value;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) unsigned int rea_riscv_abi_variadic(int count, ...)
|
||||
{
|
||||
va_list arguments;
|
||||
unsigned int folded = 0;
|
||||
va_start(arguments, count);
|
||||
for (int index = 0; index < count; index += 1) {
|
||||
double value = va_arg(arguments, double);
|
||||
folded ^= ((volatile unsigned int *)&value)[1];
|
||||
folded ^= (unsigned int)va_arg(arguments, int);
|
||||
}
|
||||
va_end(arguments);
|
||||
return folded;
|
||||
}
|
||||
|
||||
/* Volatile so the indirect call through this typed pointer is not folded. */
|
||||
float (*volatile rea_riscv_abi_callback)(float, float, int) = rea_riscv_abi_pick;
|
||||
|
||||
__attribute__((noinline)) unsigned int rea_riscv_abi_caller(int sel)
|
||||
{
|
||||
float picked = rea_riscv_abi_pick(1.5f, 2.5f, sel);
|
||||
double wide = rea_riscv_abi_wide(3.25, sel);
|
||||
double mixed = rea_riscv_abi_mixed(sel, 4.75, 5.5f, 6.125);
|
||||
double split = rea_riscv_abi_split(1, 2, 3, 4, 5, 6, sel, 7.25);
|
||||
struct rea_riscv_abi_pair pair = rea_riscv_abi_pair((struct rea_riscv_abi_pair){ 8.5f, 9.5f }, 10.5f);
|
||||
struct rea_riscv_abi_tagged tagged = rea_riscv_abi_tagged((struct rea_riscv_abi_tagged){ 11.5f, 12 }, sel);
|
||||
struct rea_riscv_abi_large large = rea_riscv_abi_large((struct rea_riscv_abi_large){ 13.5, 14.5, 15.5f }, 16.5f);
|
||||
long double quad = rea_riscv_abi_quad(22.5L, sel);
|
||||
unsigned int folded = rea_riscv_abi_variadic(2, 17.5, sel, 18.5, 19);
|
||||
float called = rea_riscv_abi_callback(20.5f, 21.5f, sel);
|
||||
return *(unsigned int *)&picked ^ ((unsigned int *)&wide)[1] ^ ((unsigned int *)&mixed)[1] ^
|
||||
((unsigned int *)&split)[1] ^ *(unsigned int *)&pair.x ^ (unsigned int)tagged.tag ^
|
||||
((unsigned int *)&large.second)[1] ^ ((unsigned int *)&quad)[3] ^ folded ^ *(unsigned int *)&called;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) unsigned int rea_riscv_entry(unsigned int selector)
|
||||
{
|
||||
return rea_riscv_probe(rea_riscv_isa_probe(rea_riscv_abi_caller((int)selector)) + rea_riscv_global);
|
||||
}
|
||||
@@ -5,11 +5,15 @@ binaries and Ghidra projects are never committed.
|
||||
|
||||
- `../c/riscv.c` and `riscv-probe.S` produce source-owned RV32/RV64 ELF images
|
||||
with double-float ABI declarations and an independently known instruction.
|
||||
`npm run verify:ghidra:riscv` exercises real CLI/MCP discovery, decompilation,
|
||||
output schemas, profile and snapshot identity, refusal diagnostics, immutable
|
||||
source bytes, and owned cleanup. It requires BYO Ghidra 12.1.4/JDK 21+ and
|
||||
RISC-V Clang/LLD/LLVM readelf; `REA_RISCV_CLANG`, `REA_RISCV_LLD` and
|
||||
`REA_RISCV_READELF` select existing tool commands.
|
||||
`../c/riscv-float-abi.c`, `riscv-probe.S` and `riscv-isa-probe.S` produce
|
||||
typed and stripped RV32 ilp32 images whose calls cover every rule of the ilp32
|
||||
policy; `riscv-checks.mjs` holds that policy's expected storage and the
|
||||
call-site oracle. `npm run verify:ghidra:riscv` exercises real CLI/MCP
|
||||
discovery, decompilation, argument storage, output schemas, profile and
|
||||
snapshot identity, admission and refusal diagnostics, immutable source bytes,
|
||||
and owned cleanup. It requires BYO Ghidra 12.1.4/JDK 21+ and RISC-V
|
||||
Clang/LLD/LLVM readelf/LLVM objdump; `REA_RISCV_CLANG`, `REA_RISCV_LLD`,
|
||||
`REA_RISCV_READELF` and `REA_RISCV_OBJDUMP` select existing tool commands.
|
||||
|
||||
- `ReaDecodeProbe.java` decodes source-owned instruction bytes from a
|
||||
language's default context and checks each mnemonic, length and optional
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
export type RiscvWord =
|
||||
| number
|
||||
| "frame"
|
||||
| { readonly frame: readonly number[] };
|
||||
export type RiscvAbiParameter = readonly [
|
||||
name: string,
|
||||
type: string,
|
||||
storage: string,
|
||||
words?: Readonly<Record<string, RiscvWord>>,
|
||||
literal?: string,
|
||||
];
|
||||
export const RISCV_ILP32_ABI: Readonly<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
readonly return: readonly [string, string];
|
||||
readonly parameters: readonly RiscvAbiParameter[];
|
||||
readonly variadic?: Readonly<Record<string, RiscvWord>>;
|
||||
}
|
||||
>
|
||||
>;
|
||||
export const RISCV_ILP32_CALLBACK: {
|
||||
readonly words: Readonly<Record<string, number>>;
|
||||
readonly literals: readonly string[];
|
||||
};
|
||||
export interface RiscvCallSite {
|
||||
readonly target: string | undefined;
|
||||
readonly words: Readonly<
|
||||
Record<string, { word?: number; stack?: number } | undefined>
|
||||
>;
|
||||
/** Caller memory at the call, by offset from the caller's incoming `sp`. */
|
||||
readonly memory: ReadonlyMap<number, { word?: number; stack?: number }>;
|
||||
}
|
||||
export function riscvStorageLocations(storage: string): readonly string[];
|
||||
export function assertRiscvIlp32TableConsistent(): void;
|
||||
export function parseRiscvCallSites(listing: string): readonly RiscvCallSite[];
|
||||
export function inspectRiscvCallSites(path: string): Promise<{
|
||||
command: string;
|
||||
version: string | undefined;
|
||||
calls: readonly RiscvCallSite[];
|
||||
}>;
|
||||
export function assertRiscvIlp32CallSites(
|
||||
calls: readonly RiscvCallSite[],
|
||||
): void;
|
||||
export function assertRiscvIlp32Storage(
|
||||
boundaries: Record<string, unknown>,
|
||||
options?: { readonly stripped?: boolean },
|
||||
): void;
|
||||
export function assertRiscvIlp32Caller(
|
||||
pseudocode: string,
|
||||
addresses: Readonly<Record<string, string>>,
|
||||
options?: { readonly stripped?: boolean },
|
||||
): void;
|
||||
@@ -0,0 +1,512 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
/**
|
||||
* The ilp32 storage of each function in tests/conformance/c/riscv-float-abi.c,
|
||||
* as Ghidra prints it, with the words `rea_riscv_abi_caller`'s call site puts
|
||||
* there. Ghidra joins a register pair most-significant word first. A word is
|
||||
* an unsigned 32-bit value, "frame" for the address of a caller local, or
|
||||
* `{ frame: [...] }` for such an address whose memory holds those words. A
|
||||
* literal is how the typed caller's pseudocode should print the argument.
|
||||
*/
|
||||
export const RISCV_ILP32_ABI = {
|
||||
rea_riscv_abi_pick: {
|
||||
return: ["float", "a0:4"],
|
||||
parameters: [
|
||||
["a", "float", "a0:4", { a0: 0x3fc00000 }, "1.5"],
|
||||
["b", "float", "a1:4", { a1: 0x40200000 }, "2.5"],
|
||||
["sel", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
rea_riscv_abi_wide: {
|
||||
return: ["double", "a1:4,a0:4"],
|
||||
parameters: [
|
||||
["a", "double", "a1:4,a0:4", { a1: 0x400a0000, a0: 0 }, "3.25"],
|
||||
["sel", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
// A named double needs no aligned pair, so d starts in a1.
|
||||
rea_riscv_abi_mixed: {
|
||||
return: ["double", "a1:4,a0:4"],
|
||||
parameters: [
|
||||
["i", "int", "a0:4"],
|
||||
["d", "double", "a2:4,a1:4", { a2: 0x40130000, a1: 0 }, "4.75"],
|
||||
["f", "float", "a3:4", { a3: 0x40b00000 }, "5.5"],
|
||||
["e", "double", "a5:4,a4:4", { a5: 0x40188000, a4: 0 }, "6.125"],
|
||||
],
|
||||
},
|
||||
// Only a7 is left for x: its low word goes there, its high word on the stack.
|
||||
rea_riscv_abi_split: {
|
||||
return: ["double", "a1:4,a0:4"],
|
||||
parameters: [
|
||||
["a", "int", "a0:4", { a0: 1 }, "1"],
|
||||
["b", "int", "a1:4", { a1: 2 }, "2"],
|
||||
["c", "int", "a2:4", { a2: 3 }, "3"],
|
||||
["d", "int", "a3:4", { a3: 4 }, "4"],
|
||||
["e", "int", "a4:4", { a4: 5 }, "5"],
|
||||
["f", "int", "a5:4", { a5: 6 }, "6"],
|
||||
["g", "int", "a6:4"],
|
||||
[
|
||||
"x",
|
||||
"double",
|
||||
"Stack[0x0]:4,a7:4",
|
||||
{ "Stack[0x0]": 0x401d0000, a7: 0 },
|
||||
"7.25",
|
||||
],
|
||||
],
|
||||
},
|
||||
// Two floats in 8 bytes: an integer pair, not fa0 and fa1.
|
||||
rea_riscv_abi_pair: {
|
||||
return: ["rea_riscv_abi_pair", "a1:4,a0:4"],
|
||||
parameters: [
|
||||
[
|
||||
"value",
|
||||
"rea_riscv_abi_pair",
|
||||
"a1:4,a0:4",
|
||||
{ a1: 0x41180000, a0: 0x41080000 },
|
||||
"(rea_riscv_abi_pair)0x4118000041080000",
|
||||
],
|
||||
["scale", "float", "a2:4", { a2: 0x41280000 }, "10.5"],
|
||||
],
|
||||
},
|
||||
rea_riscv_abi_tagged: {
|
||||
return: ["rea_riscv_abi_tagged", "a1:4,a0:4"],
|
||||
parameters: [
|
||||
[
|
||||
"value",
|
||||
"rea_riscv_abi_tagged",
|
||||
"a1:4,a0:4",
|
||||
{ a1: 12, a0: 0x41380000 },
|
||||
"(rea_riscv_abi_tagged)0xc41380000",
|
||||
],
|
||||
["tag", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
// Over 8 bytes: passed and returned through caller-allocated memory.
|
||||
rea_riscv_abi_large: {
|
||||
return: ["rea_riscv_abi_large *", "a0:4"],
|
||||
parameters: [
|
||||
[
|
||||
"__return_storage_ptr__",
|
||||
"rea_riscv_abi_large *",
|
||||
"a0:4 (auto)",
|
||||
{ a0: "frame" },
|
||||
],
|
||||
[
|
||||
"value",
|
||||
"rea_riscv_abi_large *",
|
||||
"a1:4",
|
||||
{ a1: { frame: [0, 0x402b0000, 0, 0x402d0000, 0x41780000] } },
|
||||
],
|
||||
["scale", "float", "a2:4", { a2: 0x41840000 }, "16.5"],
|
||||
],
|
||||
},
|
||||
// A long double is 16 bytes: passed and returned through caller memory too.
|
||||
rea_riscv_abi_quad: {
|
||||
return: ["long double *", "a0:4"],
|
||||
parameters: [
|
||||
[
|
||||
"__return_storage_ptr__",
|
||||
"long double *",
|
||||
"a0:4 (auto)",
|
||||
{ a0: "frame" },
|
||||
],
|
||||
[
|
||||
"value",
|
||||
"long double *",
|
||||
"a1:4",
|
||||
{ a1: { frame: [0, 0, 0, 0x40036800] } },
|
||||
],
|
||||
["sel", "int", "a2:4"],
|
||||
],
|
||||
},
|
||||
// Variadic doubles take aligned pairs, a2:a3 and a6:a7, so a1 and a5 are
|
||||
// skipped; the last int follows on the stack. Ghidra does not type them.
|
||||
rea_riscv_abi_variadic: {
|
||||
return: ["uint", "a0:4"],
|
||||
parameters: [["count", "int", "a0:4", { a0: 2 }, "2"]],
|
||||
variadic: {
|
||||
a2: 0,
|
||||
a3: 0x40318000,
|
||||
a6: 0,
|
||||
a7: 0x40328000,
|
||||
"Stack[0x0]": 19,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** The indirect call through the typed pointer `rea_riscv_abi_callback`. */
|
||||
export const RISCV_ILP32_CALLBACK = {
|
||||
words: { a0: 0x41a40000, a1: 0x41ac0000 },
|
||||
literals: ["20.5", "21.5"],
|
||||
};
|
||||
|
||||
const LOCATION_ORDER = [
|
||||
"a0",
|
||||
"a1",
|
||||
"a2",
|
||||
"a3",
|
||||
"a4",
|
||||
"a5",
|
||||
"a6",
|
||||
"a7",
|
||||
"Stack[0x0]",
|
||||
];
|
||||
const FLOAT_REGISTER = /\bf[ast]\d+\b/u;
|
||||
|
||||
/** Locations named by a Ghidra storage string, such as `Stack[0x0]:4,a7:4`. */
|
||||
export function riscvStorageLocations(storage) {
|
||||
return storage
|
||||
.replace(/ \(auto\)$/u, "")
|
||||
.split(",")
|
||||
.map((piece) => {
|
||||
const match = /^(.+):\d+$/u.exec(piece);
|
||||
assert.ok(match !== null, `Unexpected storage ${storage}`);
|
||||
return match[1];
|
||||
});
|
||||
}
|
||||
|
||||
const ordered = (locations) =>
|
||||
[...locations].sort(
|
||||
(left, right) =>
|
||||
LOCATION_ORDER.indexOf(left) - LOCATION_ORDER.indexOf(right),
|
||||
);
|
||||
|
||||
/** How the decompiler prints a call-site word: small values in decimal. */
|
||||
const printed = (word) =>
|
||||
typeof word !== "number"
|
||||
? undefined
|
||||
: word < 10
|
||||
? String(word)
|
||||
: `0x${word.toString(16)}`;
|
||||
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
// Any argument the table does not pin, including one with a cast.
|
||||
const WILDCARD = "(?:\\([^()]*\\))?[^,()]+";
|
||||
|
||||
/** Every call site word sits where the function's storage says it does. */
|
||||
export function assertRiscvIlp32TableConsistent() {
|
||||
for (const [name, { parameters }] of Object.entries(RISCV_ILP32_ABI))
|
||||
for (const [parameter, , storage, words] of parameters)
|
||||
if (words !== undefined)
|
||||
assert.deepEqual(
|
||||
ordered(Object.keys(words)),
|
||||
ordered(riscvStorageLocations(storage)),
|
||||
`${name}(${parameter}) words and storage disagree`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruct the argument words each call in an `llvm-objdump` listing of
|
||||
* `rea_riscv_abi_caller` writes. It interprets only the instructions that
|
||||
* Clang -O0 uses to set up arguments; any other write leaves the register
|
||||
* unknown. Stack addresses are offsets from the caller's incoming `sp`.
|
||||
*/
|
||||
export function parseRiscvCallSites(listing) {
|
||||
const registers = new Map([
|
||||
["zero", { word: 0 }],
|
||||
["sp", { stack: 0 }],
|
||||
]);
|
||||
const memory = new Map();
|
||||
let written = new Set();
|
||||
const calls = [];
|
||||
const immediate = (text) => {
|
||||
const value = Number.parseInt(
|
||||
text,
|
||||
text.replace(/^-/u, "").startsWith("0x") ? 16 : 10,
|
||||
);
|
||||
assert.ok(Number.isSafeInteger(value), `Unexpected immediate ${text}`);
|
||||
return value;
|
||||
};
|
||||
const address = (operand) => {
|
||||
const match = /^(-?(?:0x)?[\da-f]+)\((\w+)\)$/u.exec(operand);
|
||||
assert.ok(match !== null, `Unexpected memory operand ${operand}`);
|
||||
const base = registers.get(match[2]);
|
||||
return base?.stack === undefined
|
||||
? undefined
|
||||
: base.stack + immediate(match[1]);
|
||||
};
|
||||
const isRegister = (operand) =>
|
||||
/^(?:zero|ra|sp|gp|tp|t[0-6]|s(?:[0-9]|1[01])|a[0-7])$/u.test(
|
||||
operand ?? "",
|
||||
);
|
||||
const set = (register, value) => {
|
||||
if (register === "zero") return;
|
||||
if (value === undefined) registers.delete(register);
|
||||
else registers.set(register, value);
|
||||
};
|
||||
for (const line of listing.split("\n")) {
|
||||
const match = /^\s*[\da-f]+:\s+([a-z.]+)(?:\s+(.*))?$/u.exec(line);
|
||||
if (match === null) continue;
|
||||
const [, mnemonic, text = ""] = match;
|
||||
const operands = text
|
||||
.replace(/\s*<[^>]*>/u, "")
|
||||
.split(",")
|
||||
.map((operand) => operand.trim())
|
||||
.filter((operand) => operand.length > 0);
|
||||
if (
|
||||
(mnemonic === "jal" && operands[0] !== "zero") ||
|
||||
mnemonic === "jalr" ||
|
||||
mnemonic === "call"
|
||||
) {
|
||||
const sp = registers.get("sp").stack;
|
||||
const words = {};
|
||||
for (const register of LOCATION_ORDER.slice(0, 8))
|
||||
words[register] = registers.get(register);
|
||||
for (const offset of written)
|
||||
if (offset >= sp)
|
||||
words[`Stack[0x${(offset - sp).toString(16)}]`] = memory.get(offset);
|
||||
calls.push({
|
||||
target: mnemonic === "jalr" ? "indirect" : /<([^>]+)>/u.exec(text)?.[1],
|
||||
words,
|
||||
memory: new Map(memory),
|
||||
});
|
||||
for (const register of [
|
||||
...LOCATION_ORDER.slice(0, 8),
|
||||
"t0",
|
||||
"t1",
|
||||
"t2",
|
||||
"t3",
|
||||
"t4",
|
||||
"t5",
|
||||
"t6",
|
||||
"ra",
|
||||
])
|
||||
registers.delete(register);
|
||||
written = new Set();
|
||||
continue;
|
||||
}
|
||||
const [target, source, third] = operands;
|
||||
switch (mnemonic) {
|
||||
case "lui":
|
||||
set(target, { word: (immediate(source) << 12) >>> 0 });
|
||||
break;
|
||||
case "li":
|
||||
set(target, { word: immediate(source) >>> 0 });
|
||||
break;
|
||||
case "mv":
|
||||
set(target, registers.get(source));
|
||||
break;
|
||||
case "addi": {
|
||||
const base = registers.get(source);
|
||||
set(
|
||||
target,
|
||||
base?.word !== undefined
|
||||
? { word: (base.word + immediate(third)) >>> 0 }
|
||||
: base?.stack !== undefined
|
||||
? { stack: base.stack + immediate(third) }
|
||||
: undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sw": {
|
||||
const offset = address(source);
|
||||
if (offset === undefined) break;
|
||||
const value = registers.get(target);
|
||||
if (value === undefined) memory.delete(offset);
|
||||
else memory.set(offset, value);
|
||||
written.add(offset);
|
||||
break;
|
||||
}
|
||||
case "lw": {
|
||||
const offset = address(source);
|
||||
set(target, offset === undefined ? undefined : memory.get(offset));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (
|
||||
!/^(?:s[bhw]|b[a-z]*|j|jr|ret)$/u.test(mnemonic) &&
|
||||
isRegister(target)
|
||||
)
|
||||
set(target, undefined);
|
||||
}
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
/** Disassemble the typed fixture's caller with an independent LLVM tool. */
|
||||
export async function inspectRiscvCallSites(path) {
|
||||
const command = process.env.REA_RISCV_OBJDUMP ?? "llvm-objdump";
|
||||
const options = {
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
timeout: 10000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
};
|
||||
const version = await execute(command, ["--version"], options);
|
||||
const { stdout, stderr } = await execute(
|
||||
command,
|
||||
[
|
||||
"-d",
|
||||
"--no-show-raw-insn",
|
||||
"--disassemble-symbols=rea_riscv_abi_caller",
|
||||
path,
|
||||
],
|
||||
options,
|
||||
);
|
||||
assert.equal(stderr.trim(), "", `objdump diagnostics: ${stderr}`);
|
||||
return {
|
||||
command,
|
||||
version: version.stdout.split("\n").find((line) => /version/iu.test(line)),
|
||||
calls: parseRiscvCallSites(stdout),
|
||||
};
|
||||
}
|
||||
|
||||
const matchesWord = (call, actual, expected) =>
|
||||
typeof expected === "number"
|
||||
? actual?.word === expected
|
||||
: actual?.stack !== undefined &&
|
||||
(expected === "frame" ||
|
||||
expected.frame.every(
|
||||
(word, index) =>
|
||||
call.memory.get(actual.stack + 4 * index)?.word === word,
|
||||
));
|
||||
|
||||
/** The compiler's call sites place every known word where the table says. */
|
||||
export function assertRiscvIlp32CallSites(calls) {
|
||||
assertRiscvIlp32TableConsistent();
|
||||
const checks = [
|
||||
...Object.entries(RISCV_ILP32_ABI).map(([name, abi]) => [
|
||||
name,
|
||||
Object.assign(
|
||||
{},
|
||||
...abi.parameters.map(([, , , words]) => words ?? {}),
|
||||
abi.variadic ?? {},
|
||||
),
|
||||
]),
|
||||
["indirect", RISCV_ILP32_CALLBACK.words],
|
||||
];
|
||||
for (const [target, words] of checks) {
|
||||
const call = calls.find((candidate) => candidate.target === target);
|
||||
assert.ok(call !== undefined, `No call site for ${target}`);
|
||||
for (const [location, expected] of Object.entries(words))
|
||||
assert.ok(
|
||||
matchesWord(call, call.words[location], expected),
|
||||
`${target}: ${location} holds ${JSON.stringify(call.words[location])}, not ${JSON.stringify(expected)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a stripped build returns: in the typed build's registers, or in none
|
||||
* when the result goes through caller memory addressed by `a0`.
|
||||
*/
|
||||
const strippedReturnStorage = (abi) =>
|
||||
abi.parameters.some(([name]) => name === "__return_storage_ptr__")
|
||||
? "<VOID>"
|
||||
: abi.return[1];
|
||||
|
||||
/**
|
||||
* Typed functions keep their names and types and use exactly the table's
|
||||
* storage. Stripped functions get one inferred word per parameter location, in
|
||||
* order, and their return storage; recovered types are not compared.
|
||||
*/
|
||||
export function assertRiscvIlp32Storage(boundaries, { stripped = false } = {}) {
|
||||
assert.deepEqual(
|
||||
Object.keys(boundaries).sort(),
|
||||
Object.keys(RISCV_ILP32_ABI).sort(),
|
||||
);
|
||||
for (const [name, want] of Object.entries(RISCV_ILP32_ABI)) {
|
||||
const boundary = boundaries[name];
|
||||
assert.equal(boundary.calling_convention, "__rea_ilp32", name);
|
||||
const parameters = [...boundary.parameters].sort(
|
||||
(left, right) => left.ordinal - right.ordinal,
|
||||
);
|
||||
for (const value of [boundary.return_type, ...parameters])
|
||||
assert.doesNotMatch(value.storage ?? "", FLOAT_REGISTER, name);
|
||||
if (stripped) {
|
||||
assert.equal(
|
||||
boundary.return_type.storage,
|
||||
strippedReturnStorage(want),
|
||||
`${name} inferred return storage`,
|
||||
);
|
||||
if (want.variadic !== undefined) continue;
|
||||
assert.deepEqual(
|
||||
parameters.map((parameter) => parameter.storage),
|
||||
ordered(
|
||||
want.parameters.flatMap(([, , storage]) =>
|
||||
riscvStorageLocations(storage),
|
||||
),
|
||||
).map((location) => `${location}:4`),
|
||||
`${name} inferred parameter storage`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
assert.deepEqual(
|
||||
[boundary.return_type.data_type, boundary.return_type.storage],
|
||||
want.return,
|
||||
`${name} return storage`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
parameters.map((parameter) => [
|
||||
parameter.name,
|
||||
parameter.data_type,
|
||||
parameter.storage,
|
||||
]),
|
||||
want.parameters.map(([parameter, type, storage]) => [
|
||||
parameter,
|
||||
type,
|
||||
storage,
|
||||
]),
|
||||
`${name} parameter storage`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller's pseudocode passes the call-site words through the corrected
|
||||
* storage: typed literals in a typed build, words in register order in a
|
||||
* stripped one, and never a floating-point register.
|
||||
*/
|
||||
export function assertRiscvIlp32Caller(
|
||||
pseudocode,
|
||||
addresses,
|
||||
{ stripped = false } = {},
|
||||
) {
|
||||
assert.doesNotMatch(pseudocode, FLOAT_REGISTER);
|
||||
for (const [name, abi] of Object.entries(RISCV_ILP32_ABI)) {
|
||||
if (abi.variadic !== undefined) continue;
|
||||
let call;
|
||||
if (stripped) {
|
||||
const words = Object.assign(
|
||||
{},
|
||||
...abi.parameters.map(([, , , values]) => values ?? {}),
|
||||
);
|
||||
const locations = ordered(
|
||||
abi.parameters.flatMap(([, , storage]) =>
|
||||
riscvStorageLocations(storage),
|
||||
),
|
||||
);
|
||||
call = `FUN_ram_0*${BigInt(addresses[name]).toString(16)}\\(${locations
|
||||
.map((location) => {
|
||||
const word = printed(words[location]);
|
||||
return word === undefined ? WILDCARD : escape(word);
|
||||
})
|
||||
.join(",")}\\)`;
|
||||
} else
|
||||
call = `${name}\\(${abi.parameters
|
||||
.map(([, , , , literal]) =>
|
||||
literal === undefined ? WILDCARD : escape(literal),
|
||||
)
|
||||
.join(",")}\\)`;
|
||||
assert.match(pseudocode, new RegExp(call, "u"), `${name} call arguments`);
|
||||
}
|
||||
assert.match(
|
||||
pseudocode,
|
||||
stripped
|
||||
? new RegExp(
|
||||
`\\)\\s*\\(${Object.values(RISCV_ILP32_CALLBACK.words)
|
||||
.map((word) => escape(printed(word)))
|
||||
.join(",")},`,
|
||||
"u",
|
||||
)
|
||||
: new RegExp(
|
||||
`\\(\\*rea_riscv_abi_callback\\)\\s*\\(${RISCV_ILP32_CALLBACK.literals
|
||||
.map(escape)
|
||||
.join(",")},${WILDCARD}\\)`,
|
||||
"u",
|
||||
),
|
||||
"indirect call arguments",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
RISCV_ILP32_ABI,
|
||||
assertRiscvIlp32CallSites,
|
||||
assertRiscvIlp32Caller,
|
||||
assertRiscvIlp32Storage,
|
||||
assertRiscvIlp32TableConsistent,
|
||||
parseRiscvCallSites,
|
||||
riscvStorageLocations,
|
||||
} from "./riscv-checks.mjs";
|
||||
|
||||
// `llvm-objdump -d --no-show-raw-insn` of rea_riscv_abi_caller in the soft-float
|
||||
// fixture, built by LLVM 19 at -O0 (tabs and runs of spaces collapsed). The real
|
||||
// lane disassembles its own build; this tests the oracle, not Ghidra.
|
||||
const LISTING = `
|
||||
11408: addi sp, sp, -0xe0
|
||||
1140a: sw ra, 0xdc(sp)
|
||||
1140c: sw s0, 0xd8(sp)
|
||||
1140e: addi s0, sp, 0xe0
|
||||
11410: sw a0, -0xc(s0)
|
||||
11414: lw a2, -0xc(s0)
|
||||
11418: lui a0, 0x3fc00
|
||||
1141c: lui a1, 0x40200
|
||||
11420: jal 0x110f4 <rea_riscv_abi_pick>
|
||||
11422: sw a0, -0x10(s0)
|
||||
11426: lw a2, -0xc(s0)
|
||||
1142a: li a0, 0x0
|
||||
1142c: sw a0, -0xd4(s0)
|
||||
11430: lui a1, 0x400a0
|
||||
11434: jal 0x11130 <rea_riscv_abi_wide>
|
||||
11436: lw a4, -0xd4(s0)
|
||||
1143a: sw a1, -0x14(s0)
|
||||
1143e: sw a0, -0x18(s0)
|
||||
11442: lw a0, -0xc(s0)
|
||||
11446: lui a2, 0x40130
|
||||
1144a: lui a3, 0x40b00
|
||||
1144e: lui a5, 0x40188
|
||||
11452: mv a1, a4
|
||||
11454: jal 0x1117e <rea_riscv_abi_mixed>
|
||||
11456: lw a7, -0xd4(s0)
|
||||
1145a: sw a1, -0x1c(s0)
|
||||
1145e: sw a0, -0x20(s0)
|
||||
11462: lw a6, -0xc(s0)
|
||||
11466: mv a1, sp
|
||||
11468: lui a0, 0x401d0
|
||||
1146c: sw a0, 0x0(a1)
|
||||
1146e: li a0, 0x1
|
||||
11470: li a1, 0x2
|
||||
11472: sw a1, -0xd8(s0)
|
||||
11476: li a2, 0x3
|
||||
11478: li a3, 0x4
|
||||
1147a: li a4, 0x5
|
||||
1147c: li a5, 0x6
|
||||
1147e: jal 0x111e6 <rea_riscv_abi_split>
|
||||
11480: sw a1, -0x24(s0)
|
||||
11484: sw a0, -0x28(s0)
|
||||
11488: lui a0, 0x41080
|
||||
1148c: sw a0, -0x38(s0)
|
||||
11490: lui a0, 0x41180
|
||||
11494: sw a0, -0x34(s0)
|
||||
11498: lw a1, -0x34(s0)
|
||||
1149c: lw a0, -0x38(s0)
|
||||
114a0: lui a2, 0x41280
|
||||
114a4: jal 0x11256 <rea_riscv_abi_pair>
|
||||
114a6: sw a1, -0x2c(s0)
|
||||
114aa: sw a0, -0x30(s0)
|
||||
114ae: lui a0, 0x41380
|
||||
114b2: sw a0, -0x48(s0)
|
||||
114b6: li a0, 0xc
|
||||
114b8: sw a0, -0x44(s0)
|
||||
114bc: lw a2, -0xc(s0)
|
||||
114c0: lw a1, -0x44(s0)
|
||||
114c4: lw a0, -0x48(s0)
|
||||
114c8: jal 0x11296 <rea_riscv_abi_tagged>
|
||||
114ca: mv a2, a0
|
||||
114cc: lw a0, -0xd4(s0)
|
||||
114d0: sw a2, -0xdc(s0)
|
||||
114d4: mv a2, a1
|
||||
114d6: lw a1, -0xdc(s0)
|
||||
114da: sw a2, -0x3c(s0)
|
||||
114de: sw a1, -0x40(s0)
|
||||
114e2: lui a1, 0x402b0
|
||||
114e6: sw a1, -0x74(s0)
|
||||
114ea: sw a0, -0x78(s0)
|
||||
114ee: lui a1, 0x402d0
|
||||
114f2: sw a1, -0x6c(s0)
|
||||
114f6: sw a0, -0x70(s0)
|
||||
114fa: lui a0, 0x41780
|
||||
114fe: sw a0, -0x68(s0)
|
||||
11502: lw a0, -0x64(s0)
|
||||
11506: sw a0, -0x7c(s0)
|
||||
1150a: lw a0, -0x68(s0)
|
||||
1150e: sw a0, -0x80(s0)
|
||||
11512: lw a0, -0x6c(s0)
|
||||
11516: sw a0, -0x84(s0)
|
||||
1151a: lw a0, -0x70(s0)
|
||||
1151e: sw a0, -0x88(s0)
|
||||
11522: lw a0, -0x74(s0)
|
||||
11526: sw a0, -0x8c(s0)
|
||||
1152a: lw a0, -0x78(s0)
|
||||
1152e: sw a0, -0x90(s0)
|
||||
11532: addi a0, s0, -0x60
|
||||
11536: addi a1, s0, -0x90
|
||||
1153a: lui a2, 0x41840
|
||||
1153e: jal 0x112d2 <rea_riscv_abi_large>
|
||||
11540: lw a0, -0xd4(s0)
|
||||
11544: lw a2, -0xc(s0)
|
||||
11548: lui a1, 0x40037
|
||||
1154c: addi a1, a1, -0x800
|
||||
11550: sw a1, -0xc4(s0)
|
||||
11554: sw a0, -0xc8(s0)
|
||||
11558: sw a0, -0xcc(s0)
|
||||
1155c: sw a0, -0xd0(s0)
|
||||
11560: addi a0, s0, -0xc0
|
||||
11564: addi a1, s0, -0xd0
|
||||
11568: jal 0x11316 <rea_riscv_abi_quad>
|
||||
1156a: lw a0, -0xd8(s0)
|
||||
1156e: lw a6, -0xd4(s0)
|
||||
11572: lw a1, -0xc0(s0)
|
||||
11576: lw a2, -0xbc(s0)
|
||||
1157a: lw a3, -0xb8(s0)
|
||||
1157e: lw a4, -0xb4(s0)
|
||||
11582: sw a4, -0x94(s0)
|
||||
11586: sw a3, -0x98(s0)
|
||||
1158a: sw a2, -0x9c(s0)
|
||||
1158e: sw a1, -0xa0(s0)
|
||||
11592: lw a4, -0xc(s0)
|
||||
11596: mv a2, sp
|
||||
11598: li a1, 0x13
|
||||
1159a: sw a1, 0x0(a2)
|
||||
1159c: lui a3, 0x40318
|
||||
115a0: lui a7, 0x40328
|
||||
115a4: mv a2, a6
|
||||
115a6: jal 0x1136a <rea_riscv_abi_variadic>
|
||||
115a8: sw a0, -0xa4(s0)
|
||||
115ac: lui a0, 0x12
|
||||
115b0: lw a3, 0x648(a0)
|
||||
115b4: lw a2, -0xc(s0)
|
||||
115b8: lui a0, 0x41a40
|
||||
115bc: lui a1, 0x41ac0
|
||||
115c0: jalr a3
|
||||
115c2: sw a0, -0xa8(s0)
|
||||
115c6: lw a0, -0x10(s0)
|
||||
115ca: lw a1, -0x14(s0)
|
||||
115ce: xor a0, a0, a1
|
||||
115d0: lw a1, -0x1c(s0)
|
||||
115d4: xor a0, a0, a1
|
||||
115d6: lw a1, -0x24(s0)
|
||||
115da: xor a0, a0, a1
|
||||
115dc: lw a1, -0x30(s0)
|
||||
115e0: xor a0, a0, a1
|
||||
115e2: lw a1, -0x3c(s0)
|
||||
115e6: xor a0, a0, a1
|
||||
115e8: lw a1, -0x54(s0)
|
||||
115ec: xor a0, a0, a1
|
||||
115ee: lw a1, -0x94(s0)
|
||||
115f2: xor a0, a0, a1
|
||||
115f4: lw a1, -0xa4(s0)
|
||||
115f8: xor a0, a0, a1
|
||||
115fa: lw a1, -0xa8(s0)
|
||||
115fe: xor a0, a0, a1
|
||||
11600: lw ra, 0xdc(sp)
|
||||
11602: lw s0, 0xd8(sp)
|
||||
11604: addi sp, sp, 0xe0
|
||||
11606: ret
|
||||
`;
|
||||
|
||||
// Ghidra 12.1.4 HighFunction prototypes of the same build, printed as
|
||||
// `name return | parameter:type@storage`. DERIVED uses __rea_ilp32. The two
|
||||
// stock lines are Ghidra's ilp32d prototype, and the ilp32 prototype before it
|
||||
// passed values over 8 bytes by reference.
|
||||
const DERIVED = `
|
||||
rea_riscv_abi_pick float@a0:4 | a:float@a0:4 | b:float@a1:4 | sel:int@a2:4
|
||||
rea_riscv_abi_wide double@a1:4,a0:4 | a:double@a1:4,a0:4 | sel:int@a2:4
|
||||
rea_riscv_abi_mixed double@a1:4,a0:4 | i:int@a0:4 | d:double@a2:4,a1:4 | f:float@a3:4 | e:double@a5:4,a4:4
|
||||
rea_riscv_abi_split double@a1:4,a0:4 | a:int@a0:4 | b:int@a1:4 | c:int@a2:4 | d:int@a3:4 | e:int@a4:4 | f:int@a5:4 | g:int@a6:4 | x:double@Stack[0x0]:4,a7:4
|
||||
rea_riscv_abi_pair rea_riscv_abi_pair@a1:4,a0:4 | value:rea_riscv_abi_pair@a1:4,a0:4 | scale:float@a2:4
|
||||
rea_riscv_abi_tagged rea_riscv_abi_tagged@a1:4,a0:4 | value:rea_riscv_abi_tagged@a1:4,a0:4 | tag:int@a2:4
|
||||
rea_riscv_abi_large rea_riscv_abi_large *@a0:4 | __return_storage_ptr__:rea_riscv_abi_large *@a0:4 (auto) | value:rea_riscv_abi_large *@a1:4 | scale:float@a2:4
|
||||
rea_riscv_abi_quad long double *@a0:4 | __return_storage_ptr__:long double *@a0:4 (auto) | value:long double *@a1:4 | sel:int@a2:4
|
||||
rea_riscv_abi_variadic uint@a0:4 | count:int@a0:4
|
||||
`;
|
||||
const DEFAULT_PICK =
|
||||
"rea_riscv_abi_pick float@fa0:4 | a:float@fa0:4 | b:float@fa1:4 | sel:int@a0:4";
|
||||
const JOINED_QUAD =
|
||||
"rea_riscv_abi_quad long double *@a0:4 | __return_storage_ptr__:long double *@a0:4 (auto) | value:long double@a4:4,a3:4,a2:4,a1:4 | sel:int@a5:4";
|
||||
const STRIPPED = `
|
||||
rea_riscv_abi_pick undefined4@a0:4 | param_1:undefined4@a0:4 | param_2:undefined4@a1:4 | param_3:int@a2:4
|
||||
rea_riscv_abi_wide undefined8@a1:4,a0:4 | param_1:undefined4@a0:4 | param_2:undefined4@a1:4 | param_3:int@a2:4
|
||||
rea_riscv_abi_mixed undefined8@a1:4,a0:4 | param_1:int@a0:4 | param_2:undefined4@a1:4 | param_3:undefined4@a2:4 | param_4:undefined4@a3:4 | param_5:undefined4@a4:4 | param_6:undefined4@a5:4
|
||||
rea_riscv_abi_split undefined8@a1:4,a0:4 | param_1:uint@a0:4 | param_2:uint@a1:4 | param_3:uint@a2:4 | param_4:uint@a3:4 | param_5:uint@a4:4 | param_6:uint@a5:4 | param_7:uint@a6:4 | param_8:undefined4@a7:4 | param_9:undefined4@Stack[0x0]:4
|
||||
rea_riscv_abi_pair undefined8@a1:4,a0:4 | param_1:undefined4@a0:4 | param_2:undefined4@a1:4 | param_3:undefined4@a2:4
|
||||
rea_riscv_abi_tagged undefined8@a1:4,a0:4 | param_1:undefined4@a0:4 | param_2:undefined4@a1:4 | param_3:undefined4@a2:4
|
||||
rea_riscv_abi_large void@<VOID> | param_1:undefined4 *@a0:4 | param_2:undefined4 *@a1:4 | param_3:undefined4@a2:4
|
||||
rea_riscv_abi_quad void@<VOID> | param_1:undefined4 *@a0:4 | param_2:undefined4 *@a1:4 | param_3:undefined4@a2:4
|
||||
rea_riscv_abi_variadic uint@a0:4 | param_1:int@a0:4
|
||||
`;
|
||||
|
||||
/** Rebuild inspect_native_api boundaries from the recorded prototype lines. */
|
||||
const boundaries = (text: string, calling_convention = "__rea_ilp32") =>
|
||||
Object.fromEntries(
|
||||
text
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => {
|
||||
const [head = "", ...parameters] = line.split(" | ");
|
||||
const name = head.slice(0, head.indexOf(" "));
|
||||
const [data_type, storage] = head.slice(name.length + 1).split("@");
|
||||
return [
|
||||
name,
|
||||
{
|
||||
calling_convention,
|
||||
return_type: { data_type, storage },
|
||||
parameters: parameters.map((parameter, ordinal) => {
|
||||
const at = parameter.indexOf(":");
|
||||
const [type, place] = parameter.slice(at + 1).split("@");
|
||||
return {
|
||||
ordinal,
|
||||
name: parameter.slice(0, at),
|
||||
data_type: type,
|
||||
storage: place,
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
// Call lines of rea_riscv_abi_caller's pseudocode from the same Ghidra runs.
|
||||
const TYPED_CALLER = `
|
||||
local_10 = rea_riscv_abi_pick(1.5,2.5,sel);
|
||||
local_18 = rea_riscv_abi_wide(3.25,local_c);
|
||||
local_20 = rea_riscv_abi_mixed(local_c,4.75,5.5,6.125);
|
||||
local_28 = rea_riscv_abi_split(1,2,3,4,5,6,local_c,7.25);
|
||||
local_30 = rea_riscv_abi_pair((rea_riscv_abi_pair)0x4118000041080000,10.5);
|
||||
local_40 = rea_riscv_abi_tagged((rea_riscv_abi_tagged)0xc41380000,local_c);
|
||||
rea_riscv_abi_large(&rStack_60,&local_90,16.5);
|
||||
rea_riscv_abi_quad((long_double *)&local_c0,(long_double *)&local_d0,local_c);
|
||||
local_a4 = rea_riscv_abi_variadic(2,0x13,0,0x40318000,local_c);
|
||||
fVar1 = (*rea_riscv_abi_callback)(20.5,21.5,local_c);
|
||||
`;
|
||||
const DEFAULT_CALLER = `
|
||||
fVar2 = rea_riscv_abi_pick(in_fa0,in_fa1,0x3fc00000);
|
||||
dVar3 = rea_riscv_abi_wide((double)CONCAT44(extraout_var,fVar2),0);
|
||||
(*rea_riscv_abi_callback)(extraout_fa0,extraout_fa1_00,0x41a40000);
|
||||
`;
|
||||
const STRIPPED_CALLER = `
|
||||
local_10 = FUN_ram_000110f4(0x3fc00000,0x40200000,param_1);
|
||||
local_18 = FUN_ram_00011130(0,0x400a0000,local_c);
|
||||
local_20 = FUN_ram_0001117e(local_c,0,0x40130000,0x40b00000,0,0x40188000);
|
||||
local_28 = FUN_ram_000111e6(1,2,3,4,5,6,local_c,0,0x401d0000);
|
||||
local_30 = FUN_ram_00011256(0x41080000,0x41180000,0x41280000);
|
||||
local_40 = FUN_ram_00011296(0x41380000,0xc,local_c);
|
||||
FUN_ram_000112d2(auStack_60,&local_90,0x41840000);
|
||||
FUN_ram_00011316(&local_c0,&local_d0,local_c);
|
||||
local_a4 = FUN_ram_0001136a(2,0x13,0,0x40318000,local_c);
|
||||
uVar1 = (*(code *)PTR_FUN_ram_000110f4_ram_00012648)
|
||||
(0x41a40000,0x41ac0000,local_c,PTR_FUN_ram_000110f4_ram_00012648);
|
||||
`;
|
||||
const ADDRESSES = {
|
||||
rea_riscv_abi_pick: "0x110f4",
|
||||
rea_riscv_abi_wide: "0x11130",
|
||||
rea_riscv_abi_mixed: "0x1117e",
|
||||
rea_riscv_abi_split: "0x111e6",
|
||||
rea_riscv_abi_pair: "0x11256",
|
||||
rea_riscv_abi_tagged: "0x11296",
|
||||
rea_riscv_abi_large: "0x112d2",
|
||||
rea_riscv_abi_quad: "0x11316",
|
||||
rea_riscv_abi_variadic: "0x1136a",
|
||||
};
|
||||
|
||||
describe("RISC-V ilp32 conformance checks", () => {
|
||||
it("keeps every call-site word at its storage location", () => {
|
||||
expect(() => assertRiscvIlp32TableConsistent()).not.toThrow();
|
||||
expect(riscvStorageLocations("Stack[0x0]:4,a7:4")).toEqual([
|
||||
"Stack[0x0]",
|
||||
"a7",
|
||||
]);
|
||||
expect(riscvStorageLocations("a0:4 (auto)")).toEqual(["a0"]);
|
||||
});
|
||||
|
||||
it("recovers the words the compiler's call sites write", () => {
|
||||
const calls = parseRiscvCallSites(LISTING);
|
||||
expect(calls.map((call) => call.target)).toEqual([
|
||||
"rea_riscv_abi_pick",
|
||||
"rea_riscv_abi_wide",
|
||||
"rea_riscv_abi_mixed",
|
||||
"rea_riscv_abi_split",
|
||||
"rea_riscv_abi_pair",
|
||||
"rea_riscv_abi_tagged",
|
||||
"rea_riscv_abi_large",
|
||||
"rea_riscv_abi_quad",
|
||||
"rea_riscv_abi_variadic",
|
||||
"indirect",
|
||||
]);
|
||||
expect(() => assertRiscvIlp32CallSites(calls)).not.toThrow();
|
||||
// The variadic pairs skip a1, which still holds the stack argument.
|
||||
expect(calls[8]?.words).toMatchObject({
|
||||
a1: { word: 19 },
|
||||
a3: { word: 0x40318000 },
|
||||
a7: { word: 0x40328000 },
|
||||
"Stack[0x0]": { word: 19 },
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses a listing whose call site places a value elsewhere", () => {
|
||||
const moved = LISTING.replace("lui a1, 0x40200", "lui a2, 0x40200");
|
||||
expect(() => assertRiscvIlp32CallSites(parseRiscvCallSites(moved))).toThrow(
|
||||
/rea_riscv_abi_pick: a1/u,
|
||||
);
|
||||
// The long double copy that a1 points at must hold 22.5L's words.
|
||||
const copied = LISTING.replace("lui a1, 0x40037", "lui a1, 0x40038");
|
||||
expect(() =>
|
||||
assertRiscvIlp32CallSites(parseRiscvCallSites(copied)),
|
||||
).toThrow(/rea_riscv_abi_quad: a1/u);
|
||||
});
|
||||
|
||||
it("accepts the derived prototypes and refuses Ghidra's stock ilp32d model", () => {
|
||||
expect(() => assertRiscvIlp32Storage(boundaries(DERIVED))).not.toThrow();
|
||||
expect(() =>
|
||||
assertRiscvIlp32Storage(boundaries(STRIPPED), { stripped: true }),
|
||||
).not.toThrow();
|
||||
for (const stock of [DEFAULT_PICK, JOINED_QUAD])
|
||||
expect(() =>
|
||||
assertRiscvIlp32Storage({
|
||||
...boundaries(DERIVED),
|
||||
...boundaries(stock),
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
assertRiscvIlp32Storage(boundaries(DERIVED, "__stdcall")),
|
||||
).toThrow();
|
||||
expect(Object.keys(boundaries(DERIVED))).toEqual(
|
||||
Object.keys(RISCV_ILP32_ABI),
|
||||
);
|
||||
});
|
||||
|
||||
it("checks a stripped build's return storage, not its recovered types", () => {
|
||||
const stripped = (text: string) =>
|
||||
assertRiscvIlp32Storage(boundaries(text), { stripped: true });
|
||||
expect(() =>
|
||||
stripped(STRIPPED.replaceAll("undefined8@", "ulonglong@")),
|
||||
).not.toThrow();
|
||||
// A register-pair return that loses its high word is refused.
|
||||
expect(() =>
|
||||
stripped(
|
||||
STRIPPED.replace(
|
||||
"rea_riscv_abi_wide undefined8@a1:4,a0:4",
|
||||
"rea_riscv_abi_wide undefined4@a0:4",
|
||||
),
|
||||
),
|
||||
).toThrow(/rea_riscv_abi_wide inferred return storage/u);
|
||||
});
|
||||
|
||||
it("reads the caller's arguments through the corrected storage", () => {
|
||||
expect(() => assertRiscvIlp32Caller(TYPED_CALLER, ADDRESSES)).not.toThrow();
|
||||
expect(() =>
|
||||
assertRiscvIlp32Caller(STRIPPED_CALLER, ADDRESSES, { stripped: true }),
|
||||
).not.toThrow();
|
||||
expect(() => assertRiscvIlp32Caller(DEFAULT_CALLER, ADDRESSES)).toThrow();
|
||||
expect(() =>
|
||||
assertRiscvIlp32Caller(
|
||||
STRIPPED_CALLER.replace(
|
||||
"0x40130000,0x40b00000",
|
||||
"0x40b00000,0x40130000",
|
||||
),
|
||||
ADDRESSES,
|
||||
{ stripped: true },
|
||||
),
|
||||
).toThrow(/rea_riscv_abi_mixed call arguments/u);
|
||||
});
|
||||
});
|
||||
@@ -8,19 +8,108 @@ import { promisify } from "node:util";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
/** One build per float ABI the lane verifies. */
|
||||
const BUILDS = {
|
||||
double: {
|
||||
sources: ["../c/riscv.c", "./riscv-probe.S"],
|
||||
march: (bits) => `rv${bits}imafdc`,
|
||||
mabi: (bits) => (bits === 32 ? "ilp32d" : "lp64d"),
|
||||
flags: 5,
|
||||
},
|
||||
// ESP-IDF's ESP32-C2 and C3 base string, built ilp32 as ESP-IDF does.
|
||||
soft: {
|
||||
sources: [
|
||||
"../c/riscv-float-abi.c",
|
||||
"./riscv-probe.S",
|
||||
"./riscv-isa-probe.S",
|
||||
],
|
||||
march: () => "rv32imc_zicsr_zifencei",
|
||||
mabi: () => "ilp32",
|
||||
flags: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const readerOptions = {
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
timeout: 10000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
};
|
||||
|
||||
/** Independent reading of LLVM readelf's `-h -S -s -A` report. */
|
||||
const readRiscvElf = async (reader, path, floatAbi) => {
|
||||
const { stdout, stderr } = await execute(
|
||||
reader,
|
||||
["-h", "-S", "-s", "-A", "-W", path],
|
||||
readerOptions,
|
||||
);
|
||||
assert.equal(stderr.trim(), "", "Unexpected ELF reader diagnostics");
|
||||
const field = (expression, label) => {
|
||||
const matches = [...stdout.matchAll(expression)];
|
||||
assert.equal(matches.length, 1, `Missing or ambiguous readelf ${label}`);
|
||||
return matches[0][1].trim();
|
||||
};
|
||||
const sections = [
|
||||
...stdout.matchAll(
|
||||
/^\s*\[\s*\d+\]\s+(\S+)\s+(\S+)\s+([\da-f]+)\s+([\da-f]+)\s+([\da-f]+)\s+[\da-f]+\s+(\S*)\s+\d+\s+\d+\s+\d+\s*$/gmu,
|
||||
),
|
||||
].map(([, name, type, address, offset, size, flags]) => ({
|
||||
name,
|
||||
type,
|
||||
address: Number.parseInt(address, 16),
|
||||
offset: Number.parseInt(offset, 16),
|
||||
size: Number.parseInt(size, 16),
|
||||
allocated: flags.includes("A"),
|
||||
}));
|
||||
assert.ok(sections.length > 0, "Missing readelf section table");
|
||||
// LLVM readelf prints each attribute as Tag, TagName and Value lines.
|
||||
const arch = field(/TagName: arch\s*\n\s*Value: (\S+)\s*$/gmu, "arch");
|
||||
const [base, ...tokens] = arch.split("_");
|
||||
const declared = /^rv(32|64)([ie])/u.exec(base);
|
||||
assert.ok(declared !== null, `Unexpected Tag_RISCV_arch ${arch}`);
|
||||
return {
|
||||
stdout,
|
||||
sections,
|
||||
class: field(/^\s*Class:\s*(.+)$/gmu, "class"),
|
||||
data: field(/^\s*Data:\s*(.+)$/gmu, "byte order"),
|
||||
type: field(/^\s*Type:\s*(.+)$/gmu, "type"),
|
||||
machine: field(/^\s*Machine:\s*(.+)$/gmu, "machine"),
|
||||
flags: Number(field(/^\s*Flags:\s*(0x[\da-f]+).*$/gmu, "flags")),
|
||||
isa: {
|
||||
status: "declared",
|
||||
arch,
|
||||
xlen: Number(declared[1]),
|
||||
base: declared[2],
|
||||
extensions: tokens.map((token) => /^(.*?)(?:\d+p\d+)?$/u.exec(token)[1]),
|
||||
},
|
||||
floatAbi,
|
||||
};
|
||||
};
|
||||
|
||||
const sha256 = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
|
||||
/** Build and independently inspect a source-owned ELF; never execute it. */
|
||||
export async function buildRiscvFixture(directory, bits) {
|
||||
export async function buildRiscvFixture(directory, bits, floatAbi = "double") {
|
||||
assert.ok(bits === 32 || bits === 64);
|
||||
const build = BUILDS[floatAbi];
|
||||
assert.ok(build !== undefined, `No ${floatAbi}-float RISC-V fixture`);
|
||||
assert.ok(floatAbi === "double" || bits === 32);
|
||||
const compiler = process.env.REA_RISCV_CLANG ?? "clang";
|
||||
const linker = process.env.REA_RISCV_LLD ?? "ld.lld";
|
||||
const reader = process.env.REA_RISCV_READELF ?? "llvm-readelf";
|
||||
const source = fileURLToPath(new URL("../c/riscv.c", import.meta.url));
|
||||
const probe = fileURLToPath(new URL("./riscv-probe.S", import.meta.url));
|
||||
const path = join(directory, `rv${bits}.elf`);
|
||||
const sources = build.sources.map((path) =>
|
||||
fileURLToPath(new URL(path, import.meta.url)),
|
||||
);
|
||||
const path = join(
|
||||
directory,
|
||||
`rv${bits}${floatAbi === "double" ? "" : `-${floatAbi}`}.elf`,
|
||||
);
|
||||
const args = [
|
||||
`--target=riscv${bits}-unknown-elf`,
|
||||
`-march=rv${bits}imafdc`,
|
||||
`-mabi=${bits === 32 ? "ilp32d" : "lp64d"}`,
|
||||
`-march=${build.march(bits)}`,
|
||||
`-mabi=${build.mabi(bits)}`,
|
||||
`--ld-path=${linker}`,
|
||||
"-nostdlib",
|
||||
"-static",
|
||||
@@ -29,8 +118,7 @@ export async function buildRiscvFixture(directory, bits) {
|
||||
"-g",
|
||||
"-Wl,-e,rea_riscv_entry",
|
||||
"-Wl,--build-id=none",
|
||||
source,
|
||||
probe,
|
||||
...sources,
|
||||
"-o",
|
||||
path,
|
||||
];
|
||||
@@ -39,46 +127,37 @@ export async function buildRiscvFixture(directory, bits) {
|
||||
const linkerVersion = await execute(linker, ["--version"], options);
|
||||
const readerVersion = await execute(reader, ["--version"], options);
|
||||
await execute(compiler, args, { ...options, timeout: 60000 });
|
||||
const { stdout, stderr } = await execute(reader, ["-h", "-s", "-W", path], {
|
||||
...options,
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
});
|
||||
assert.equal(stderr.trim(), "", "Unexpected ELF reader diagnostics");
|
||||
const field = (expression, label) => {
|
||||
const matches = [...stdout.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"), `ELF${bits}`);
|
||||
assert.match(field(/^\s*Data:\s*(.+)$/gmu, "byte order"), /little endian/u);
|
||||
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
|
||||
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^RISC-V\b/u);
|
||||
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+).*$/gmu, "flags"));
|
||||
assert.equal(flags, 5, "Compiler did not declare RVC and double-float ABI");
|
||||
const elf = await readRiscvElf(reader, path, floatAbi);
|
||||
assert.equal(elf.class, `ELF${bits}`);
|
||||
assert.match(elf.data, /little endian/u);
|
||||
assert.match(elf.type, /^EXEC\b/u);
|
||||
assert.match(elf.machine, /^RISC-V\b/u);
|
||||
assert.equal(
|
||||
elf.flags,
|
||||
build.flags,
|
||||
`Compiler did not declare RVC and the ${floatAbi}-float ABI`,
|
||||
);
|
||||
assert.equal(elf.isa.xlen, bits);
|
||||
// Every source-owned global; the verifier asserts the ones it needs.
|
||||
const symbols = {};
|
||||
for (const name of [
|
||||
"rea_riscv_entry",
|
||||
"rea_riscv_leaf",
|
||||
"rea_riscv_float",
|
||||
"rea_riscv_probe",
|
||||
"rea_riscv_global",
|
||||
]) {
|
||||
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",
|
||||
),
|
||||
name,
|
||||
);
|
||||
for (const [, value, name] of elf.stdout.matchAll(
|
||||
/^\s*\d+:\s+([\da-f]+)\s+\d+\s+(?:FUNC|OBJECT)\s+GLOBAL\s+\S+\s+\d+\s+(rea_riscv_\w+)\s*$/gmu,
|
||||
)) {
|
||||
assert.equal(symbols[name], undefined, `Ambiguous readelf symbol ${name}`);
|
||||
symbols[name] = `0x${BigInt(`0x${value}`).toString(16)}`;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
sha256: createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
riscv: { elfClass: bits, byteOrder: "little", type: 2, flags },
|
||||
sha256: await sha256(path),
|
||||
riscv: {
|
||||
elfClass: bits,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: elf.flags,
|
||||
isa: elf.isa,
|
||||
},
|
||||
symbols,
|
||||
sections: elf.sections,
|
||||
toolchain: {
|
||||
compiler: { command: compiler, version: compilerVersion.stdout.trim() },
|
||||
linker: { command: linker, version: linkerVersion.stdout.trim() },
|
||||
@@ -86,12 +165,69 @@ export async function buildRiscvFixture(directory, bits) {
|
||||
arguments: args,
|
||||
},
|
||||
sources: await Promise.all(
|
||||
[source, probe].map(async (sourcePath) => ({
|
||||
sources.map(async (sourcePath) => ({
|
||||
path: sourcePath,
|
||||
sha256: createHash("sha256")
|
||||
.update(await readFile(sourcePath))
|
||||
.digest("hex"),
|
||||
sha256: await sha256(sourcePath),
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Link the same objects again without symbols or DWARF. Every allocated
|
||||
* section must keep its address and bytes, so the typed build's symbol
|
||||
* addresses locate the same code in the stripped image.
|
||||
*/
|
||||
export async function stripRiscvFixture(directory, typed) {
|
||||
const path = join(
|
||||
directory,
|
||||
typed.path.split("/").at(-1).replace(".elf", "-stripped.elf"),
|
||||
);
|
||||
const args = [
|
||||
...typed.toolchain.arguments.slice(0, -2),
|
||||
"-Wl,--strip-all",
|
||||
"-o",
|
||||
path,
|
||||
];
|
||||
await execute(typed.toolchain.compiler.command, args, {
|
||||
timeout: 60000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const elf = await readRiscvElf(typed.toolchain.reader.command, path, "soft");
|
||||
assert.equal(elf.flags, typed.riscv.flags);
|
||||
assert.deepEqual(elf.isa, typed.riscv.isa);
|
||||
assert.ok(
|
||||
!elf.sections.some(
|
||||
(section) =>
|
||||
section.name === ".symtab" || section.name.startsWith(".debug"),
|
||||
),
|
||||
"Stripped fixture still carries symbols or DWARF",
|
||||
);
|
||||
const allocated = (sections) =>
|
||||
sections
|
||||
.filter((section) => section.allocated)
|
||||
.map(({ name, address, size }) => ({ name, address, size }));
|
||||
assert.deepEqual(allocated(elf.sections), allocated(typed.sections));
|
||||
const [typedBytes, strippedBytes] = await Promise.all([
|
||||
readFile(typed.path),
|
||||
readFile(path),
|
||||
]);
|
||||
for (const section of elf.sections.filter((value) => value.allocated)) {
|
||||
if (section.type === "NOBITS") continue;
|
||||
const twin = typed.sections.find((value) => value.name === section.name);
|
||||
assert.ok(
|
||||
strippedBytes
|
||||
.subarray(section.offset, section.offset + section.size)
|
||||
.equals(typedBytes.subarray(twin.offset, twin.offset + twin.size)),
|
||||
`Stripped ${section.name} bytes differ from the typed build`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...typed,
|
||||
path,
|
||||
sha256: await sha256(path),
|
||||
stripped: true,
|
||||
sections: elf.sections,
|
||||
toolchain: { ...typed.toolchain, arguments: args },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/* Zifencei and Zicsr oracle for the soft-float fixture, independent of the
|
||||
* compiler's C code generation. Returns its argument unchanged. */
|
||||
.section .text
|
||||
.option norvc
|
||||
.global rea_riscv_isa_probe
|
||||
.type rea_riscv_isa_probe, @function
|
||||
rea_riscv_isa_probe:
|
||||
fence.i
|
||||
csrr t0, mcycle
|
||||
ret
|
||||
.size rea_riscv_isa_probe, .-rea_riscv_isa_probe
|
||||
Reference in new issue
Block a user