rea function, end to end
One rea function call opens the file, binds Ghidra, imports a private copy, asks one question over a socket, and prints one Evidence record in about eight seconds.
You typed rea function fixtures/ledger-arm64-O1 _percent_change --provider ghidra --format json. About eight seconds later, 1881 lines of JSON arrived on stdout, and the exit status was 0. Somewhere in there is the answer to your question about before == 0, next to a decompiler artifact that the source never had.
When you finish this section, you can read every top-level field of that record and say which part of the system wrote it. You can also point at the one place where Ghidra's pseudocode and your C disagree.
The command
$ rea function fixtures/ledger-arm64-O1 _percent_change --provider ghidra --format json
[exit 0] (… s) -> 06-function-percent-change.jsonfunction takes a path and a procedure, and it runs the MCP operation analyze_function with { procedure } as its parameters. --provider ghidra names the engine, and --format json and --filter-output are read at the executable boundary before the command runs. Without --format, the terminal output is TOON, and exit 0 means that the operation finished. The result can still hold partial evidence, warnings, or open questions. Figure 1.2 follows the call through the system.
From the command to the bridge
- The session parses the Mach-O header, reads the architecture, and hashes the file. The record's
subjectcarries the result:mach-o,arm64, and the SHA-256 that00-fixtures.txtlists forledger-arm64-O1. - The registry selects
ghidra, because the command asked for it, and recordsselection_source: "request". - The provider builds the analysis profile, a list of versioned contract names such as
function_body_evidence: "complete-inclusive-ranges-v1", and hashes it. The digest3fc72c9b…in the record is this hash. - The client makes a private runtime root and allocates
bridge.sock. It copies the file astarget-<sha256[0:12]>.binwhile hashing it again. Then it launchesjavawithghidra.Ghidra ghidra.app.util.headless.AnalyzeHeadless,-import,-readOnly, and-deleteProject. The copy istarget-01b5d08b08fb.bin, and the overview record shows that name as itsdocument(the capture). The startup deadline is 330,000 ms. - The client sends
ping. It accepts the session only whenrun_id, the token, the profile digest, the target SHA-256, andanalysis_completeall match. - The client writes one JSON line that asks for
analyze_function. Ghidra runs the analysis and writes one JSON line back. - rea adds the operation's limitation sentences to the answer and stores the result as one Evidence record.
- The client sends
shutdownwith a 10 s deadline, and Ghidra deletes the project.
The record, field by field
{
"evidence_id": "ev_b492c85e…",
"subject": {
"name": "ledger-arm64-O1",
"digest": {
"sha256": "01b5d08b…"
},
"local_path": "$CAPTURE/fixtures/ledger-arm64-O1",
"format": "mach-o",
"architecture": "arm64"
},
"provider": {
"id": "ghidra",
"name": "Ghidra",
"version": "12.1.4"
},
…evidence_id hashes the semantic content and leaves out subject.name and subject.local_path, so the same bytes under another path give the same id. provider names the engine and its version. The next field binds the answer to the engine's settings:
"analysis_profile": {
"provider": {
"id": "ghidra",
"name": "Ghidra",
"version": "12.1.4"
},
"parameters": {
"target_kind": "executable",
"target_format": "mach-o",
"architecture": "arm64",
…
"import_mode": "ephemeral-source-immutable",
…
"loader": "auto-from-header",
…
},
"digest": "3fc72c9b…"
},
"predicate_type": "rea.analysis",
"operation": "analyze_function",
"parameters": {
"procedure": "_percent_change"
},Analysis profile: the provider's committed settings, hashed over RFC 8785 canonical JSON. Change one contract version and the digest changes, which is how rea refuses an old snapshot (section 5.3). predicate_type is the default tag rea.analysis, and operation is the MCP name. parameters is the caller's input, and it is part of the identity.
raw_result holds the provider-native dossier under value with its own empty limitations, and normalized_result holds the same dossier as the canonical result. confidence is observed and authority is shipped-artifact, the defaults for a static read of the file on disk. environment is null, because nothing ran.
limitations has ten strings: four that every Ghidra operation carries, five for analyze_function, and one for macOS about the native decompiler (the captures). locations and evidence_links are empty. relationship_coverage has the status complete, and its one diagnostic says that only resolved Ghidra call references are returned. Figure 1.3 lays the 16 fields out.
Inside normalized_result
Procedure: the function identity. _percent_change is at 0x100000500, its body is one range from 0x100000500 to 0x100000517 inclusive, 24 bytes, and contains_entry is true. The FunctionManager signature reads undefined _percent_change(), because Ghidra's symbol carries no types. The decompiler's view under native_api reads ulong _percent_change(ulong param_1,int param_2) with confidence: "low" on each parameter. Your source says int percent_change(int before, int after). Both are observations, as the limitation says: "Types and calling conventions are Ghidra decompiler observations, not source declarations."
"assembly": [
"0x100000500: cbz w0, 0x100000514",
"0x100000504: sub w8, w1, w0",
"0x100000508: mov w9, #0x64",
"0x10000050c: mul w8, w8, w9",
"0x100000510: sdiv w0, w8, w0",
"0x100000514: ret"
],Six instructions of four bytes each make the 24 bytes. cbz w0 is your if (before == 0) return 0;, with before in w0, and mov w9, #0x64 is the 100. The pseudocode tells a longer story:
ulong _percent_change(ulong param_1,int param_2)
{
uint uVar1;
int iVar2;
iVar2 = (int)param_1;
if (iVar2 != 0) {
uVar1 = 0;
if (iVar2 != 0) {
uVar1 = ((param_2 - iVar2) * 100) / iVar2;
}
param_1 = (ulong)uVar1;
}
return param_1;
}The outer if is your guard. The inner if is not in your source. native_value_flow shows where it comes from. The operations 0x100000510#7, #8, #9, and #10 are a COPY of the constant 0, an INT_NOTEQUAL, a CBRANCH, and an INT_SDIV. All four carry the address of the sdiv instruction. Ghidra's model of sdiv gives 0 when the divisor is 0, and the decompiler printed that model as a second check.
Both checks are correct, and one is redundant, and only the source tells you which. Section 4.6 and the lifting lesson go deeper.
References: the dossier lists two incoming references. One is an UNCONDITIONAL_CALL from 0x1000006a0 inside the function Ghidra names entry at 0x1000005d0, 368 bytes long. The other is a DATA reference from 0x100008118 with no owning procedure, inside the __LINKEDIT segment that the overview lists from 0x100008000 (the capture). The one outgoing reference is the CONDITIONAL_JUMP of cbz. callers names entry, and callees is empty, because the function calls nothing. unresolved_calls is empty, and facet_availability marks all eight facets of the dossier as available.
Basic blocks: three, at 0x100000500, 0x100000504, and 0x100000514, with exclusive end values. The body range uses inclusive ends, as its contract name complete-inclusive-ranges-v1 says. Keep the two conventions apart when you compare addresses.
native_value_flow holds 13 p-code operations and 13 def-use edges with truncated: false. Every operation carries is_dead: true, and the limitation explains that the flag mirrors the Java AST and that block_membership reports the real membership. Read the limitations before the numbers, every time.
rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10