3.2

inspect-binary-layout and the load image

On macOS, inspect-binary-layout refuses with capability_unavailable, and inspect-macho is the engine-free reader of segments, sections, load commands, imports, and exports.

You ran rea inspect-binary-layout fixtures/ledger-x86_64-O1.o --format json on the capture Mac. It exited 1 and printed a JSON error instead of a layout. Nothing is broken. rea 6.7.0 has two engine-free layout readers, and this one runs on Linux x64 only.

When you finish this section, you can read that error envelope and name the host rule behind it. You can also read the load image of ledger-arm64-O1 from inspect-macho: four segments, 17 load commands, six imports, and six exports.

The error envelope

inspect-binary-layout on macOSjsonFull log
{
  "code": "capability_unavailable",
  "category": "unsupported_provider",
  "message": "The initial real-verified pwntools artifact profile supports Linux x64 only.",
  "retryable": false,
  "remediation": {
    "action": "The initial real-verified pwntools artifact profile supports Linux x64 only."
  },
  "details": {
    "provider_id": "pwntools-elf",
    "operation": "inspect_binary_layout",
    "reason": "The initial real-verified pwntools artifact profile supports Linux x64 only."
  }
}
The whole file. The CLI exit status was 1.

Every failure in rea has this shape: code, category, message, retryable, remediation.action, and an optional details object. The provider is pwntools-elf. Its decoder throws AnalysisCapabilityUnavailableError when process.platform is not linux or process.arch is not x64, before it looks for Python. The message is a constant in the same file. The MCP server lists the tool as unavailable unless the host is Linux x64 and REA_PWNTOOLS_PYTHON is an absolute path. The exit status follows the CLI rule for an operation that could not finish.

The capture kit gave the tool the ELF object because inspect_binary_layout is written for ELF. On a Linux x64 host with pwntools 4.15.0 it returns sections, segments, symbols, and relocations. Section 2.3 shows how each operation declares its availability before you call it, and the reference lists every error code the captures produced.

inspect-macho and the tools it runs

inspect-macho is the macOS reader, from the provider native-macos, "macOS native inspection utilities". It runs seven host commands and records each one as provenance.

the provenance of one inspect-macho runjsonFull log
"provenance": [
  {
    "tool": "lipo",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/lipo",
      "-detailed_info",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "otool",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/llvm-otool",
      "-h",
      "-l",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "nm",
    …
  },
  {
    "tool": "dyld_info",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/dyld_info",
      "-imports",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "dyld_info",
    …
  },
  {
    "tool": "dwarfdump",
    …
  },
  {
    "tool": "vtool",
    …
  }
],
Cut to the tool names and three of the commands. Each entry also records the executable SHA-256, the exit code, and the output sizes.

The command list is fixed in the provider. It runs lipo -detailed_info to discover the slices. For each slice it runs otool -h -l, nm -gjU --no-dyldinfo, dyld_info -imports, dyld_info -exports, and dwarfdump --uuid. It runs vtool -show-build as an optional extra. The run starts no Java and takes a few seconds. Keep one limitation of the record in mind when a name you expect is missing: "Imports and exports combine dyld_info and nm; stripped or toolchain-hidden symbols may be absent."

Fig. 3.2two layout commands on the capture Maccomparison
Two layout commands on the capture Mac Left: rea inspect-binary-layout on ledger-x86_64-O1.o reaches the pwntools-elf provider, which answers capability_unavailable with category unsupported_provider because its profile supports Linux x64 only, and the command exits 1. Right: rea inspect-macho on ledger-arm64-O1 reaches the native-macos provider, which runs lipo, otool, nm, dyld_info, dwarfdump, and vtool, found through xcrun, and returns an Evidence record with 17 load commands and 4 segments, exit 0. A · INSPECT-BINARY-LAYOUT B · INSPECT-MACHO rea inspect-binary-layout fixtures/ledger-x86_64-O1.o provider pwntools-elf needs Linux x64 and REA_PWNTOOLS_PYTHON capability_unavailable unsupported_provider · exit 1 rea inspect-macho fixtures/ledger-arm64-O1 otool nm dyld_info dwarfdump and lipo, vtool, found through xcrun Evidence ev_497834aa 17 load commands · 4 segments · exit 0
The same host refuses one layout reader on platform and answers the other with 17 load commands, because each reader names its own prerequisites. Left to right in each column. Solid arrows are calls, the dashed rose arrow is the failure, the dashed ink arrow is a reply.

The load image

header facts and the entry pointjsonFull log
"format": "mach-o",
"selected_architecture": {
  "name": "arm64",
  "container_file_offset": 0
},
"endian": "little",
"word_size": 64,
"file_type": "2",
"flags": [
  "0x00200085"
],
"uuid": "59A1CCA5-97D1-3661-99DA-666BC2BB38D7",
"entrypoints": {
  "items": [
    {
      "file_offset": 1488
    }
  ],
  …
},
…
"build_metadata": {
  "items": [
    {
      "platform": "MACOS",
      "minimum_os": "26.0",
      "sdk": "26.2",
      …
Cut from normalized_result to the header, the entry point, and the build metadata. rea names the build platform and tool codes from loader.h.

Load command: one record in the list that follows the Mach-O header, with a cmd type and a cmdsize. Apple publishes no prose specification, so the loader.h header in the XNU source is the reference. file_type 2 is MH_EXECUTE. The flags 0x00200085 are MH_NOUNDEFS 0x1, MH_DYLDLINK 0x4, MH_TWOLEVEL 0x80, and MH_PIE 0x200000 added together. The entry point comes from LC_MAIN, whose entryoff is 1488, or 0x5d0. Add it to the __TEXT address below and you get 0x1000005d0, the _main export.

The four LC_SEGMENT_64 commands are the load image. Each segment_command_64 names a vmaddr, a vmsize, a fileoff, a filesize, two protection fields, and nsects, and its section_64 records follow inside the same command.

Segmentvmaddrvmsizefileofffilesizeinitprotnsects
__PAGEZERO0x042949672960000
__TEXT0x100000000163840163845, read and execute5
__DATA_CONST0x1000040001638416384163843, read and write2
__LINKEDIT0x100008000163843276811041, read0
the __TEXT segment and its five sectionsjsonFull log
{
  "name": "__TEXT",
  "vm_address": "0x100000000",
  "vm_size": 16384,
  "file_offset": 0,
  "file_size": 16384,
  …
  "sections": {
    "items": [
      {
        "segment": "__TEXT",
        "name": "__text",
        "address": "0x100000500",
        "size": 576,
        "file_offset": 1280,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__stubs",
        "address": "0x100000740",
        "size": 48,
        "file_offset": 1856,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__cstring",
        "address": "0x100000770",
        "size": 95,
        "file_offset": 1904,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__const",
        "address": "0x1000007d0",
        "size": 64,
        "file_offset": 2000,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__unwind_info",
        "address": "0x100000810",
        "size": 96,
        "file_offset": 2064,
        …
      }
    ],
Cut from the segments collection. Permissions, alignment, and flags are cut from each section.

__PAGEZERO reserves the first 4 GiB of the address space with no file bytes, so a null pointer never reaches mapped memory. __TEXT starts at 0x100000000 and maps the first 16384 bytes of the file. Its code section __text holds 576 bytes at 0x100000500, and Part 4 accounts for every one of them. __cstring holds the format strings and the five labels, and __const holds the two lookup tables that the jump table section reads.

The __LINKEDIT segment has no sections. Other load commands point into it by file offset.

CommandOffset in the fileSize
LC_DYLD_CHAINED_FIXUPS32768176
LC_DYLD_EXPORTS_TRIE32944104
LC_FUNCTION_STARTS330488
LC_SYMTAB, 13 symbolssymoff 33056, stroff 33304strsize 152
LC_CODE_SIGNATURE33456416

The last command ends at 33872, the file size. In ELF the loader reads a program header table and the linker reads a section header table, two tables that the ELF header points to. In Mach-O there is one list, and LC_SEGMENT_64 carries its sections inside the command. Lesson 32 builds a reader for both formats from the specifications and lines up each load command with its ELF idea.

Fig. 3.3the load image of ledger-arm64-O1layers
The load image of ledger-arm64-O1 Four LC_SEGMENT_64 commands drawn as rows from low to high address. __PAGEZERO at 0x0 spans 4 GiB with no file bytes. __TEXT at 0x100000000 maps file bytes 0 to 16384 and holds __text, __stubs, __cstring, __const and __unwind_info, and LC_MAIN points at entryoff 1488 inside it. __DATA_CONST at 0x100004000 maps bytes 16384 to 32768 and holds __got and __const. __LINKEDIT at 0x100008000 maps bytes 32768 to 33872 and holds the chained fixups, the exports trie, the function starts, the symbol and string tables, and the code signature. ADDRESS SEGMENT AND SECTIONS FILE OFFSET 0x0 __PAGEZERO · 4 GiB reserved vmsize 4294967296 · filesize 0 · prot 0 none 0x100000000 __TEXT · r-x __text 0x100000500 · 576 B · file 1280 __stubs 0x100000740 · 48 B · file 1856 __cstring 0x100000770 · 95 B · file 1904 __const 0x1000007d0 · 64 B · file 2000 __unwind_info 0x100000810 · 96 B · file 2064 LC_MAIN entryoff 1488 · entry at 0x1000005d0 0 to 16384 0x100004000 __DATA_CONST · rw- __got 0x100004000 · 48 B · file 16384 __const 0x100004030 · 40 B · file 16432 16384 to 32768 0x100008000 __LINKEDIT · r-- · no sections chained fixups file 32768 · 176 B exports trie file 32944 · 104 B function starts file 33048 · 8 B symtab file 33056 · 13 symbols · strtab file 33304 code signature file 33456 · 416 B 32768 to 33872
Four segments map 33872 file bytes into three address ranges, and every other load command points into the last one by file offset. Read from the lowest address at the top. The left column is the address, the right column is the file offset. The dashed box has no file bytes.

Imports and exports

the exported functionsjsonFull log
"exports": {
  "items": [
    …
    {
      "name": "_grade",
      "address": "0x100000518",
      …
    },
    {
      "name": "_grade_label",
      "address": "0x100000588",
      …
    },
    {
      "name": "_main",
      "address": "0x1000005d0",
      …
    },
    {
      "name": "_percent_change",
      "address": "0x100000500",
      …
    },
    {
      "name": "_total",
      "address": "0x100000554",
      …
    }
  ],
  "total": null,
  "exhaustive": false,
  "limitations": [
    "Merged nm/dyld_info results may be incomplete for stripped binaries."
  ]
},
Cut from the exports collection. The __mh_execute_header entry and the weak and reexport fields are cut.

Six exports: the five functions and __mh_execute_header at 0x100000000. Six imports from libSystem: ___stack_chk_fail, ___stack_chk_guard, ___stderrp, _atoi, _fwrite, and _printf.

One dependency: /usr/lib/libSystem.B.dylib. rea lists only dylib load commands as dependencies. The dynamic linker /usr/lib/dyld stays in the LC_LOAD_DYLINKER row of the load commands.

Both the imports and the exports collections say exhaustive: false, because each merges the text of two tools. The record does not promise that the list is complete, and you should not promise it either. Section 4.2 shows the same names as Ghidra sees them, and the next section turns 0x100000518 into a file offset.

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