Thomas Piacentini
← Workshop

Chapter

listprocedures and function bodies

Original text · English

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 source
"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.
Original source
title: five of the thirteen procedures
source: capture/out/10-mcp-05-list-procedures.json
lang: json
note: Cut from normalized_result: one external function, two internal functions, and one thunk. The other nine entries have the same shape.
---
"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"
    }
  },
  …
],

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.".

Address Name Kind Where it comes from
EXTERNAL:0x1 to EXTERNAL:0x4 ___stack_chk_fail, _atoi, _fwrite, _printf external four of the six libSystem imports in section 3.2
0x100000500 _percent_change internal, 24 bytes ledger.c
0x100000518 _grade internal, 60 bytes ledger.c
0x100000554 _total internal, 52 bytes ledger.c
0x100000588 _grade_label internal, 72 bytes ledger.c
0x1000005d0 entry internal, 368 bytes main in ledger-main.c, named by LC_MAIN
0x100000740 to 0x100000764 the same four names thunk, 12 bytes each the __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 {{native §Exact CLI selectors}}, so rea function fixtures/ledger-arm64-O1 _main works.

Fig. 4.3the thirteen procedures of ledger-arm64-O1structure
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.
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. From capture/out/10-mcp-05-list-procedures.json and the five dossiers.

Original SVG

Original source
id: fig-4-3
kind: structure
title: the thirteen procedures of ledger-arm64-O1
claim: 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.
caption: 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. From capture/out/10-mcp-05-list-procedures.json and the five dossiers.

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 source
"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.
Original source
title: the procedure block of the _grade dossier
source: capture/out/06-function-grade.json
lang: json
note: Cut from normalized_result. The pseudocode and every later facet are cut.
---
"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": []
},

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:

Original source
label: names before addresses
source: native §Exact CLI selectors
---
When a selector matches an exact symbol name in the database, the name wins over a bare hexadecimal address.

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.

Original source
- Count procedures as internal, thunk, and external before you compare two builds.
- Read the `__stubs` size and divide by the thunk count to find the stub size.
- Add the `total_bytes` of the internal bodies and compare with the code section size.
- Treat `end` in `body.ranges` as inclusive and every other `end` as exclusive.

Sources: capture/out/10-mcp-05-list-procedures.json, capture/out/06-function-grade.json, capture/out/10-mcp-09-analyze-function-percent-change-O1.json, capture/out/10-mcp-11-analyze-function-total-O1.json, capture/out/10-mcp-12-analyze-function-grade-label-O1.json, capture/out/04-inspect-macho.json; rea 6.7.0 at 27dc42d: bridge/ghidra/ReaGhidraBridge.java, src/ghidra/GhidraProviderCapabilities.ts; rea docs/native-investigation.md

Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓