mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 13:56:06 +00:00
* 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
284 lines
14 KiB
Java
284 lines
14 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|
|
}
|