* 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
23 KiB
RISC-V ELF analysis through Ghidra
This lane extends native ELF analysis for EM_RISCV targets. It does not
execute RISC-V code, emulate a boot sequence, or rehost firmware. It addresses
#2063 and follows the same
lane shape as generic MIPS support.
Admission boundary
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, 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 RISC-V images are not supported.
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_DOUBLEuse Ghidra's stock double-float model unchanged. Their.riscv.attributesis not read for admission. - ELF32 images that declare the soft-float ABI (
e_flagsfloat-ABI bits0, ilp32) use REA's ilp32 calling-convention policy. They must also declare an RV32I base and only decoded extensions inTag_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, 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 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
dynamic dependencies are not inspected, and the session records that boundary as
a limitation rather than inferring a load model.
Why the language is pinned
Ghidra's header-based selection is not reliable for RV32. The 12.1.4
riscv.opinion pins variant="default" for the 64-bit constraint and leaves the
32-bit constraint unpinned with the comment that the loader "must choose between
'default' and 'AndeStar' variants". Measured against the installed release tree:
| Fixture | Ghidra auto-selection |
|---|---|
RV64 ELF, e_flags 0x5 (RVC, double-float ABI) |
RISCV:LE:64:default:gcc |
RV32 ELF, e_flags 0x5, no Andes declaration |
RISCV:LE:32:AndeStar_v5:gcc |
So an ordinary RV32 image lands on an Andes processor model unless the language
is stated. REA therefore pins RISCV:LE:32:default or RISCV:LE:64:default
with the gcc compiler specification per class, records
language_selection: provider-pinned-v1, and refuses a caller-selected
REA_GHIDRA_LANGUAGE_ID or REA_GHIDRA_COMPILER_SPEC_ID that disagrees with the
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.
- Choice.
- Admit little-endian ELF32
ET_EXECimages 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:defaultlanguage andgcccompiler specification. After analysis,ReaGhidraRiscvCallingConvention.javaderives the program-specific__rea_ilp32prototype from the program's default prototype as the ilp32 policy 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_archdeclaration with an RV32I base and only extensions that Ghidra 12.1.4's generic RV32 language implements with p-code (ISA declarations). - Soft-float profiles add
float_abiandarchtoriscv_elf. Double-float profiles keep their exact parameters, so their digests, sessions and snapshots are unchanged. - ilp32f keeps its written policy and stays refused until its prototype is implemented and verified. Quad-float, RVE, TSO, and ELF64 soft- or single-float images stay refused.
- Admit little-endian ELF32
- Alternatives (and what switching to each would take):
- Pin the deprecated soft-float languages (
RISCV:LE:32:RV32IMCand others,riscv32.cspec): REA's language catalog would have to readold/, the variants cover only I, IC and IMC, Ghidra marks them deprecated, and no stock specification models ilp32f. - Assign
__rea_ilp32only 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 claimfa0. 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_abiandarchto 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.
- Pin the deprecated soft-float languages (
- Semantic owner and consumers.
- Owners:
src/domain/riscvIsaDeclaration.tsowns the float-ABI vocabulary ofe_flagsand the parsing of.riscv.attributesandTag_RISCV_arch.GHIDRA_RISCV_LANES,GHIDRA_RISCV_CALLING_CONVENTIONSandGHIDRA_RISCV32_DECODED_EXTENSIONS(src/ghidra/GhidraRiscvProfile.ts) own admission, lane ids and the policy table.bridge/ghidra/ReaGhidraRiscvCallingConvention.javaowns 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.
- Owners:
- 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.archandriscv_calling_conventionare persisted in sessions, snapshots and Evidence once released. A changed derivation ships as a new policy id, such asilp32-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,xespvandxespdsp, 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 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
Ghidra 12.1.4 PUBLIC with a 64-bit JDK 21+ is required, and on macOS the release
tree ships no host decompiler binary, so Decompiler/os/<platform>/decompile and
sleigh must come from your own build; see
installation.
Run npm run verify:ghidra:riscv with an existing Ghidra 12.1.4 installation,
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:
GHIDRA_INSTALL_DIR=/path/to/ghidra_12.1.4_PUBLIC \
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 four ET_EXEC fixtures from committed freestanding C and
assembly sources:
- RV32 and RV64 double-float builds (
rv{32,64}imafdc) ofriscv.c. - An RV32 soft-float build of
riscv-float-abi.cwith 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.iandcsrrdecode, as ESP32-C3's Zifencei and Zicsr declare.- An LLVM objdump reading of
rea_riscv_abi_callerfinds each call-site word where the ilp32 policy puts it: an unaligneddoubleina1/a2, thea7/stack split, two floats in an integer pair, a large structure and along doublepassed by reference (the pointed-to copies hold the source values), aligned variadic pairs, and an indirect call. inspect_native_apireports__rea_ilp32with 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.