4.2

list_procedures and function bodies

list_procedures returns every function Ghidra defined, internal and external, with thunks kept distinct, and the dossier body tells you how many bytes each one owns.

You wrote four functions and a main. Ghidra lists 13 procedures, four of them at addresses like EXTERNAL:0x1, and the one you called main is named entry. Each of those facts has a reason in the file, and the list is the place to learn them.

When you finish this section, you can account for all 13 procedures and tell a thunk from its external target. You can also read a function body as inclusive address ranges.

The inventory

five of the thirteen proceduresjsonFull log
"normalized_result": [
  {
    "address": "EXTERNAL:0x1",
    "value": "/usr/lib/libSystem.B.dylib::___stack_chk_fail",
    "procedure": {
      "external": true,
      "thunk": false,
      "thunk_target": null
    }
  },
  …
  {
    "address": "0x100000518",
    "value": "_grade",
    "procedure": {
      "external": false,
      "thunk": false,
      "thunk_target": null
    }
  },
  …
  {
    "address": "0x1000005d0",
    "value": "entry",
    "procedure": {
      "external": false,
      "thunk": false,
      "thunk_target": null
    }
  },
  {
    "address": "0x100000740",
    "value": "___stack_chk_fail",
    "procedure": {
      "external": false,
      "thunk": true,
      "thunk_target": "EXTERNAL:0x1"
    }
  },
  …
],
Cut from normalized_result: one external function, two internal functions, and one thunk. The other nine entries have the same shape.

The bridge builds the list once per program from two Ghidra iterators, the internal functions and the external functions. It drops duplicates by entry address. For each function it records external, thunk, and the resolved thunk_target when there is one. The value is the qualified symbol name, which is why the external entries carry their library path. The record's limitation states the rule: "External functions and local thunks are distinct; procedure metadata identifies both and preserves a thunk target when Ghidra resolves one.".

AddressNameKindWhere it comes from
EXTERNAL:0x1 to EXTERNAL:0x4___stack_chk_fail, _atoi, _fwrite, _printfexternalfour of the six libSystem imports in section 3.2
0x100000500_percent_changeinternal, 24 bytesledger.c
0x100000518_gradeinternal, 60 bytesledger.c
0x100000554_totalinternal, 52 bytesledger.c
0x100000588_grade_labelinternal, 72 bytesledger.c
0x1000005d0entryinternal, 368 bytesmain in ledger-main.c, named by LC_MAIN
0x100000740 to 0x100000764the same four namesthunk, 12 bytes eachthe __stubs section, 48 bytes

Thunk: a small function whose only job is to transfer control to another function. The four thunks sit in __stubs, the 48-byte section at 0x100000740 from section 3.2. Each one is a 12-byte stub that reaches the real libSystem function. Ghidra represents that function as an entry in its EXTERNAL block, the last segment in section 3.4. The two imports that are not functions, ___stack_chk_guard and ___stderrp, do not appear here, because they are data.

entry is _main. inspect-macho exports _main at 0x1000005d0, and LC_MAIN points there with entryoff 1488. Ghidra names the entry point entry and keeps _main as a second symbol at the same address. The docs say that a secondary symbol at a function entry, such as an imported _main label, is accepted as a selector, so rea function fixtures/ledger-arm64-O1 _main works.

Fig. 4.3the thirteen procedures of ledger-arm64-O1structure
The thirteen procedures of ledger-arm64-O1 Thirteen rows sorted by address. Four external functions at EXTERNAL:0x1 to 0x4 from libSystem. Five internal functions in __text: _percent_change at 0x100000500 with 24 bytes, _grade at 0x100000518 with 60, _total at 0x100000554 with 52, _grade_label at 0x100000588 with 72, and entry at 0x1000005d0 with 368. Four thunks of 12 bytes each in __stubs from 0x100000740, each resolved to one external. The internal bodies add up to 576 bytes, the size of __text. ADDRESS NAME BODY CLASSIFICATION EXTERNAL:0x1 ___stack_chk_fail external · libSystem.B.dylib EXTERNAL:0x2 _atoi external · libSystem.B.dylib EXTERNAL:0x3 _fwrite external · libSystem.B.dylib EXTERNAL:0x4 _printf external · libSystem.B.dylib 0x100000500 _percent_change 24 B body 0x500 to 0x517, ledger.c 0x100000518 _grade 60 B body 0x518 to 0x553, ledger.c 0x100000554 _total 52 B body 0x554 to 0x587, ledger.c 0x100000588 _grade_label 72 B body 0x588 to 0x5cf, ledger.c 0x1000005d0 entry 368 B body 0x5d0 to 0x73f · LC_MAIN 0x100000740 ___stack_chk_fail 12 B thunk to EXTERNAL:0x1 in __stubs 0x10000074c _atoi 12 B thunk to EXTERNAL:0x2 in __stubs 0x100000758 _fwrite 12 B thunk to EXTERNAL:0x3 in __stubs 0x100000764 _printf 12 B thunk to EXTERNAL:0x4 in __stubs internal bodies 24 + 60 + 52 + 72 + 368 = 576 bytes, the size of __text · stubs 4 × 12 = 48 bytes
Five internal functions fill the 576 bytes of __text without a gap, four thunks fill __stubs, and four externals live in a block Ghidra made. Read top to bottom by address. Grey rows are outside the file, green rows are code in __text, violet rows are the thunks Ghidra resolved.

Function bodies

The inventory has no sizes. The size of a function is in the procedure.body of its analyze_function dossier, and section 4.6 reads the rest of that record.

the procedure block of the _grade dossierjsonFull log
"procedure": {
  "address": "0x100000518",
  "name": "_grade",
  "classification": {
    "external": false,
    "thunk": false,
    "thunk_target": null,
    "provenance": "ghidra-function-manager"
  },
  "body": {
    "available": true,
    "provenance": "ghidra-function-body-address-set",
    "ranges": [
      {
        "start": "0x100000518",
        "end": "0x100000553"
      }
    ],
    "total_bytes": 60,
    "span_bytes": 60,
    "non_contiguous": false,
    "contains_entry": true
  },
  "signature": "undefined _grade()",
  "locals": []
},
Cut from normalized_result. The pseudocode and every later facet are cut.

Function body: the set of addresses Ghidra assigns to one function, reported as ranges with inclusive ends. _grade owns 0x100000518 to 0x100000553, 60 bytes, and _total starts at 0x100000554. The profile names this contract function_body_evidence: "complete-inclusive-ranges-v1". Three other fields in the record use exclusive ends: the segments of binary_overview, the basic_blocks of a dossier, and the memory blocks of list_segments. Read the field name before you add one.

The five internal bodies add up: 24 + 60 + 52 + 72 + 368 is 576, the size of __text. No byte of the code section belongs to two functions, and no byte belongs to none. span_bytes equals total_bytes and non_contiguous is false for all five. When they differ, the function has a gap, and the bytes in the gap are not owned by that function. The research notes a 16-bit case where a body of 503 bytes spans 517.

Selecting a procedure

Every function operation takes a procedure string. The rule for reading it:

A string with a 0x prefix or an address-space prefix is an address. Any other string is tried as a symbol name first. A name that matches more than one function fails with ambiguous and the list of entry addresses. A name that matches nothing fails with not_found, which section 4.6 shows as the MCP error invalid_request.

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