4.6

analyze_function and pseudocode

analyze_function returns one dossier with sixteen facets, and its pseudocode is a decompiler's reading that you check against the assembly and the bytes in the same record.

You asked for percent_change and got a function that returns ulong and tests iVar2 != 0 twice. The source returns int and tests once. The instructions in the same record are right, and the text around them is a reading of those instructions. Learning to tell the two apart is what this section is for.

When you finish this section, you can read the sixteen facets of a dossier. You can mark which lines of pseudocode follow from an instruction and which are a guess. You can also read the error for a name that does not exist.

The dossier

rea function is the CLI name of analyze_function. The CLI run on _grade and the MCP call in the same session produced the same evidence_id, ev_41e8ca1c and 56 more digits. The id is a hash of the record's semantic content, and the file path and the display name are left out of it. Section 5.1 reads every field of the record. This section reads its normalized_result, which has sixteen facets.

the pseudocode, the blocks, and the signature facts of _gradejsonFull log
"pseudocode": "\nundefined4 _grade(int param_1)\n\n{\n  uint uVar1;\n  \n  uVar1 = param_1 / 10 - 6;\n  if (uVar1 < 5) {\n    return *(undefined4 *)(&DAT_1000007d0 + (ulong)uVar1 * 4);\n  }\n  return 0;\n}\n\n",
…
"basic_blocks": [
  {
    "start": "0x100000518",
    "end": "0x10000053c",
    "successors": [
      "0x10000053c",
      "0x10000054c"
    ]
  },
  …
],
"native_api": {
  "available": true,
  "provenance": "ghidra-high-function",
  "signature_source": "default",
  "calling_convention": "__cdecl",
  …
      "data_type": "undefined4",
      "size_bytes": 4,
      "storage": "w0:4",
      "confidence": "low",
  …
  "jump_tables": [],
  "pseudocode": {
    "classification": "decompiler-generated-non-source",
    "compilable": false
  },
  …
  "limitations": [
    "Types and calling conventions are Ghidra decompiler observations, not source declarations.",
    "Register variables, compiler-generated blocks, and pointer arithmetic remain decompiler artifacts.",
    "Pseudocode is neither original source nor guaranteed to compile."
  ]
},
Cut from normalized_result. The procedure, assembly, reference, and value-flow facets are cut, and so are the parameter entries of native_api.

The pseudocode is one JSON string. Decoded, it reads:

undefined4 _grade(int param_1)

{
  uint uVar1;
  
  uVar1 = param_1 / 10 - 6;
  if (uVar1 < 5) {
    return *(undefined4 *)(&DAT_1000007d0 + (ulong)uVar1 * 4);
  }
  return 0;
}
FacetFor _gradeWhat it holds
procedureentry 0x100000518, body 60 bytesthe identity and body of section 4.2
pseudocodeone stringthe decompiler text
assembly15 linesthe Listing text of section 4.3
commentsemptydatabase comments at the entry
callers, calleesempty, emptyresolved functions only
incoming_references, outgoing_references1, 2typed edges, as in section 4.4
unresolved_callsemptycall sites whose destination has no known procedure
referenced_strings, referenced_namesempty, 2LAB_10000054c, DAT_1000007d0
basic_blocks3starts, exclusive ends, successors
native_api__cdecl, signature_source defaulttypes, storage, confidence, jump_tables
native_value_flow14 operations, 12 def-use edgeshigh p-code, truncated: false
facet_availabilityeight facets, all available: truewhether each facet was observed
limitations4the facet's own limits

Dossier: rea's name for the composed record of one function. The bridge builds it from five Ghidra sources: the FunctionManager, the Listing, the ReferenceManager, the BasicBlockModel, and the decompiler. The record-level limitation names all five. The record carries ten limitations, the result four more, native_api three, and native_value_flow five. Read them once. They say in rea's words what this section shows with examples.

The signature facts are the first thing to read. signature_source is default, which means no analyzer or user set the prototype, and every parameter and the return value carry confidence: "low". The bridge gives medium only when the source is analysis. native_api.pseudocode.compilable is false, and the third native_api limitation is written into the Java source itself.

Fig. 4.8the sixteen facets of the _grade dossierstructure
The sixteen facets of the _grade dossier A table of the sixteen keys of normalized_result in the analyze_function record for _grade: procedure, pseudocode, assembly, comments, callers, callees, incoming_references, outgoing_references, unresolved_calls, referenced_strings, referenced_names, basic_blocks, native_api, native_value_flow, facet_availability and limitations, each with its value for _grade and a note on what the facet holds. FACET FOR _GRADE WHAT IT HOLDS Evidence ev_41e8ca1c · analyze_function · observed procedure _grade 0x100000518 entry, classification, body 60 B pseudocode one string decompiler text, compilable false assembly 15 lines Listing text, address first comments [] database comments at the entry callers [] resolved callers only callees [] resolved callees only incoming_references 1 DATA from 0x10000811a, no owning procedure outgoing_references 2 CONDITIONAL_JUMP, DATA read unresolved_calls [] calls with no known destination referenced_strings [] strings the body reaches referenced_names 2 LAB_10000054c, DAT_1000007d0 basic_blocks 3 successors inside the body only native_api __cdecl, default signature facts, jump_tables [] native_value_flow 14 ops, 12 edges high p-code, truncated false facet_availability 8 facets, available whether each facet was observed limitations 4 + 10 on the record plus 3 in native_api, 5 in value flow
One record holds the identity, the text, the edges, the blocks, the signature facts, and the value flow of one function, each with its own limits. Read top to bottom. The left column is the facet name, the middle column is its value for _grade, the right column says what the facet holds.

procedure_pseudo_code: the text alone

rea decompile is the CLI name of procedure_pseudo_code. It returns the text and nothing else.

the pseudocode of _totaljsonFull log
"operation": "procedure_pseudo_code",
"parameters": {
  "procedure": "_total"
},
"raw_result": {
  "value": "\nint _total(int *param_1,uint param_2)\n\n{\n  int iVar1;\n  ulong uVar2;\n  \n  if (0 < (int)param_2) {\n…
  "limitations": []
},
…
"limitations": [
  …
  "Pseudocode is Ghidra decompiler output, not original source and not text-equivalent to Hopper output.",
  "External functions and functions without an analyzable body return null; other decompiler failures remain explicit.",
  …
],
Cut to the operation, the start of the text, and the two decompiler limitations of the seven.
int _total(int *param_1,uint param_2)

{
  int iVar1;
  ulong uVar2;
  
  if (0 < (int)param_2) {
    iVar1 = 0;
    uVar2 = (ulong)param_2;
    do {
      iVar1 = *param_1 + iVar1;
      uVar2 = uVar2 - 1;
      param_1 = param_1 + 1;
    } while (uVar2 != 0);
    return iVar1;
  }
  return 0;
}

The bridge opens one DecompInterface per program with C output, the syntax tree, and jump loads on, and no instruction limit. Each call is decompileFunction(function, 0, monitor), with no timeout, and an external or bodyless function returns null. The text is the getC() output of the result.

_total reads well. The do loop with uVar2 - 1 is the rotated loop that subs x9, x9, #0x1 and b.ne make in the assembly. The pointer type int *param_1 follows from the load ldr w10, [x0], #0x4. The one open question is uint param_2. The source says int count. The instructions compare w1 as signed once and then count it down in x9, so both types fit, and Ghidra picked one.

Where the text is incomplete or wrong

Decompiler artifact: a line of pseudocode that the decompiler needed to express the machine code and that no source line produced. The native_api limitations name three kinds: register variables, compiler-generated blocks, and pointer arithmetic. Three records in the captures show three kinds of gap between the text and the instructions.

_percent_change, text and instructionsjsonFull log
"pseudocode": "\nulong _percent_change(ulong param_1,int param_2)\n\n{\n  uint uVar1;\n  int iVar2;\n  \n  iVar2 = (int)param_1;\n  if (iVar2 != 0) {\n    uVar1 = 0;\n    if (iVar2 != 0) {\n      uVar1 = ((param_2 - iVar2) * 100) / iVar2;\n    }\n    param_1 = (ulong)uVar1;\n  }\n  return param_1;\n}\n\n",
"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"
],
The pseudocode string and the complete assembly array of the dossier.

Six instructions. cbz w0, 0x100000514 jumps to the ret when before is zero, and on that path w0 is zero and the function returns it. The other path computes (w1 - w0) * 100 and divides with sdiv w0, w8, w0. Both paths return 32 bits in w0.

The pseudocode says the function returns ulong, eight bytes, and returns param_1, the whole of x0. The ABI and the source say four bytes. Nothing in the text marks the difference. The record does: return_type.data_type is ulong, confidence is low, and signature_source is default. The second if (iVar2 != 0) repeats a test that one instruction made once. It changes no value, and it is not in the instructions.

Fig. 4.9percent_change as written and as decompiledcomparison
percent_change as written and as decompiled Two blocks. The top block is the four-line C source of percent_change from ledger.c. The bottom block is the fourteen-line Ghidra pseudocode of _percent_change from the analyze_function record. Three lines are marked in rose: the ulong return type where the ABI returns four bytes, the second test of iVar2 that repeats a value tested once, and the return of param_1, the whole register, where the source returns an int. LEDGER.C · WHAT YOU WROTE int percent_change(int before, int after) { if (before == 0) return 0; return (after - before) * 100 / before; } GHIDRA 12.1.4 PSEUDOCODE · _PERCENT_CHANGE 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; } ulong return, the ABI says 4 bytes second test of a value tested once returns param_1, source returns int
Four source lines became fourteen lines of pseudocode, and the three marked lines describe only the registers. Read the source first, then the pseudocode. The rose marks name the return width, the repeated test, and the return of the whole register.

_grade shows the second kind of gap. The decompiled text keeps the table as *(undefined4 *)(&DAT_1000007d0 + (ulong)uVar1 * 4). The address is right, the element size is right, and the five values are not there. Section 3.3 turns 0x1000007d0 into file offset 2000, and the 20 bytes there are 1, 2, 3, 4, 4. The return type is undefined4, four bytes of unknown meaning. The text is incomplete, and you complete it from the bytes.

The x86-64 build of grade shows the third kind. Its pseudocode has the line uVar1 = -(param_1 >> 0x1f) ^ (uint)((ulong)((long)param_1 * 0x66666667) >> 0x3f); for the default return value.

grade on x86-64, the default value as an expressionjsonFull log
"value": "\nuint grade(int param_1)\n\n{\n  uint uVar1;\n  uint uVar2;\n  \n  uVar2 = param_1 / 10 - 6;\n  uVar1 = -(param_1 >> 0x1f) ^ (uint)((ulong)((long)param_1 * 0x66666667) >> 0x3f);\n…
The start of the pseudocode string, cut after the line that writes the default value.

The instruction behind that line is XOR EAX, EAX at 0x10002a in section 4.3. The register is zero. Ghidra wrote it as the sign of param_1 exclusive-or the sign bit of the product. Those are two spellings of the same bit, and the decompiler did not cancel them. The value is 0 for every int, so the text is not false. A reader who trusts the text believes the default depends on the score, which is false.

Lesson 34 compiles this text and tests it against the source over a grid. It then puts its own lifter output beside Ghidra's and accounts for every difference in percent_change, grade, and total.

The rule from the three cases is short. The assembly facet is observed. The pseudocode is observed text about a derived reading. Check a type against the storage, a constant against the bytes, and a branch against the blocks before you repeat what the text says.

A name that does not exist

analyze_function for a procedure that is not therejsonFull log
"name": "analyze_function",
"arguments": {
  "procedure": "_nothing"
}
…
"isError": true
…
"decoded_text": {
  "error": {
    "code": "invalid_request",
    "category": "invalid_input",
    "message": "Analysis input is invalid. Check the arguments and try again.",
    "retryable": true,
    "remediation": {
      "action": "Correct the listed arguments and retry."
    },
    "details": {
      "operation": "analyze_function",
      "issues": [
        {
          "path": [],
          "reason": "invalid_value",
          "message": "Unknown Ghidra procedure name or address: _nothing"
        }
      ],
      …
    }
  }
}
The request arguments, the error flag, and the decoded error. The text content is the same JSON on one line. The diagnostics object in details is cut. It holds the run id, the temporary paths, the process id, the Ghidra output, and remote_code: "not_found".

The bridge tried _nothing as a symbol name, then as an address, and failed with not_found. The diagnostics object in the error shows the remote code not_found that the bridge sent. The client maps the remote codes invalid_request, not_found, and ambiguous to AnalysisInputError. That error projects to the code invalid_request. An error reaches the MCP client as text with isError: true and no result. The server log shows where the time went.

the failed call in the server logjsonlFull log
{"level":40,"time":…,"application":"rea","mode":"mcp","layer":"ghidra-bridge","method":"analyze_function","status":"error","msg":"Ghidra bridge request completed"}
{"level":40,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"analyze_function","durationMs":…,"status":"error","msg":"MCP tool execution completed"}
Lines 10 and 11. The first is the bridge layer, the second is the server layer. The time and durationMs values change on every run and are cut.

The lookup was a round trip on the socket to Ghidra, a couple of milliseconds in all. retryable is true: correct the name from section 4.2 and call again. The session stays open and the Ghidra project stays imported.

What to do with a dossier

Read the sixteen facets in this order. Start with procedure for the body and assembly for what the processor does. Then read basic_blocks for the shape and the references for the edges, and pseudocode last. Section 5.5 compares two dossiers of the same function from two builds. Section 7.2 shows the test that decides whether a rebuild from this text is equal to the original.

To see where each line of pseudocode comes from, run the Inside a Decompiler machine. It decompiles percent_change from the x86-64 fixture object in nine stations, from the header to the pseudocode. The instructions, IR and pseudocode on the page are the ones that the course lifter in lesson 34 prints. Switch the build to -O0 to see stack slots become locals, and switch the symbols off to see the name become an unknown.

rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10