Map SGI Indy IP24 PROM 5.3 Rev B10

Fetch and verify the four MAME indy_4610 PROM dumps, wrap the newest
dump in an ELF32 BE MIPS image for Ghidra/REA, and record the resulting
structural map: image layout, exception vector table, boot flow,
function anchors and firmware feature clusters.
This commit is contained in:
opencode.energia.arti.ee committed 2026-11-02 11:04:39 +00:00
commit 1dc99443b7
11 files changed
+2562

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
# SGI firmware dumps and generated wrappers (copyrighted, fetch on demand)
roms/*.bin
roms/*.zip
roms/*.elf
# Local REA evidence bundles, scratch output
evidence/
*.rea.backup
*.rea.backup.*
# Python bytecode
__pycache__/
*.pyc
# Editor/OS noise
.DS_Store
*.swp
+101
View File
@@ -0,0 +1,101 @@
# sgi-indy
Research workspace for the Silicon Graphics Indy (IP24) workstation, focused on
its boot PROM.
The repository contains:
- a reproducible fetcher and checksum manifest for the four MAME `indy_4610`
PROM dumps;
- a tool that wraps a raw PROM dump in a minimal ELF32 big-endian MIPS image so
it can be opened by Ghidra/REA at its real address;
- a structural map of the newest PROM, `ip24prom.070-9101-011.bin`
(SGI Version 5.3 Rev B10, 12 Feb 1996);
- the full 1,074-entry function index produced by Ghidra auto-analysis;
- notes for reproducing the REA + Ghidra setup.
## Result at a glance
| Item | Value |
| --- | --- |
| Newest Indy PROM | `ip24prom.070-9101-011.bin`, 524,288 bytes |
| Version string | `SGI Version 5.3 Rev B10 R4X00/R5000 IP24 Feb 12, 1996` |
| SHA-1 | `ac663a4db68528e400487e98cbf901f620fb30ce` (matches MAME) |
| SHA-256 | `333a0b2e39bb828a28a758b6f633ae7290030129dc747297a1be68dc317b86ae` |
| Load address | `0x1FC00000` (BIOS window) |
| Analyzed code span | `0x1FC00000` - `0x1FC4A8A0`, 1,074 functions |
| Defined strings | 1,418, from `0x1FC01B7C` to `0x1FC7C16B` |
| Used bytes | `0x00000` - `0x7F9FF`; remainder is `0xFF` padding |
See [docs/rom-map.md](docs/rom-map.md) for the memory map, exception-vector
table, boot flow and function anchors.
See [docs/rom-map.json](docs/rom-map.json) for the machine-readable layout and
[docs/procedure-index.csv](docs/procedure-index.csv) for every function start.
## ROM files are not committed
The PROM dumps are copyrighted SGI firmware. `roms/*.bin`, the MAME zip and the
generated ELF wrapper are ignored by git. Fetch and verify them with:
```bash
./tools/fetch-roms.sh
```
The fetch script downloads the MAME 0.236 `indy_4610` set and checks every SHA-1
against MAME's `src/mame/sgi/indy_indigo2.cpp`.
## Reproduce the analysis
Prerequisites: Node.js 22.23.4+, REA (`npm install -g rea-agents`), Ghidra 12.1.x
and JDK 21. This workspace used REA 6.8.0, Ghidra 12.1.2 and OpenJDK 21.
```bash
export GHIDRA_INSTALL_DIR=/opt/ghidra
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
./tools/fetch-roms.sh
./tools/make-elf-wrapper.py \
roms/ip24prom.070-9101-011.bin -o roms/indy-ip24-5.3-b10.elf
rea analyze roms/indy-ip24-5.3-b10.elf --provider ghidra --json
```
REA's headerless `raw-binary` lane does not include MIPS. The wrapper declares
ELF32, big-endian, `ET_EXEC`, `e_flags = EF_MIPS_ARCH_32` and no ABI-flags
record, which selects REA's recordless MIPS admission and pins Ghidra to
`MIPS:BE:32:default`. The ROM bytes are copied unchanged at file offset `0x1000`
and mapped at `0x1FC00000`.
To regenerate the checked-in map and function index, open the wrapper in a REA
session, export its evidence bundle, then run:
```bash
./tools/rom-map.py roms/ip24prom.070-9101-011.bin \
--evidence evidence/rea-bundle.json \
--output docs/rom-map.json \
--procedures-csv docs/procedure-index.csv
```
## Repository layout
```
docs/ map, setup notes, generated map JSON and function index
roms/ fetched ROMs (ignored) and the SHA-1 manifest
tools/ fetch, ELF wrapper, map generator
evidence/ local REA bundle (ignored)
```
## Setup notes
The Gitea remote is
<https://git.wut.ee/opencode.energia.arti.ee/sgi-indy>.
See [docs/rea-setup.md](docs/rea-setup.md) for the exact REA/OpenCode
configuration used here.
## Sources and attribution
- MAME `indy_indigo2.cpp` for the IP22/IP24 memory map and ROM identities:
<https://github.com/mamedev/mame/blob/master/src/mame/sgi/indy_indigo2.cpp>
- MAME 0.236 ROM set archive used for the dumps.
- MiSTer `SGIIndy_MiSTer` ROM directory used as a cross-check.
- REA: <https://github.com/morluto/rea>
File diff suppressed because it is too large. Load diff
+89
View File
@@ -0,0 +1,89 @@
# REA setup used for this project
[REA](https://github.com/morluto/rea) is the reverse-engineering MCP server and
CLI used to analyze the Indy PROM. This note records the exact environment so
the analysis can be reproduced.
## Installed components
| Component | Version | Location |
| --- | --- | --- |
| Node.js | 22.23.4 | `/opt/node-v22.23.4` (`node`, `npm`, `npx` symlinked into `/usr/local/bin`) |
| REA | 6.8.0 | global npm install; `rea` in `/usr/local/bin` |
| Ghidra | 12.1.2 | `/opt/ghidra` |
| JDK | OpenJDK 21.0.13 | `/usr/lib/jvm/java-21-openjdk-amd64` |
REA requires Node `^22.23.4 || ^24.22.1 || >=26.12.1`. Debian 13 ships
Node 20.19, so the official Node 22 tarball was installed under `/opt` instead
of the distribution package.
## Registration
The setup was applied for OpenCode only:
```bash
export GHIDRA_INSTALL_DIR=/opt/ghidra
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
rea setup --client opencode --dry-run --json # reviewed plan
rea setup --client opencode --yes
```
The plan had two actions and no provider installation:
- update `/home/arti/.config/opencode/opencode.json` with an MCP server entry
whose launcher is `/usr/local/bin/rea mcp`;
- install the bundled `reverse-engineer-anything` skill under
`~/.agents/skills/reverse-engineer-anything`.
Setup preserved the existing `kagi` MCP entry and wrote
`opencode.json.rea.backup`. OpenCode must be restarted before the MCP server is
available in a session; the CLI (`rea ...`) works immediately.
`rea doctor` after setup reports the Ghidra provider as available with
`GHIDRA_INSTALL_DIR=/opt/ghidra`.
The machine's SSH key is not authorized on the Gitea host, so the git remote
uses HTTPS with the configured `tea` credential helper:
```
https://git.wut.ee/opencode.energia.arti.ee/sgi-indy.git
```
## The MIPS problem
REA 6.8.0's `--target-format raw-binary` lane accepts `x86`, `x86_64`, `arm`,
`arm64`, `6502` and `m68k` only. There is no raw MIPS architecture. MIPS is
supported through the Ghidra ELF lane (`docs/ghidra-mips.md` upstream).
The Indy PROM is a headerless 512 KiB dump, so the analysis uses
`tools/make-elf-wrapper.py` to build a load-image-only ELF32 big-endian MIPS
file:
- `e_type = ET_EXEC`, `e_machine = EM_MIPS`;
- `e_flags = 0x50000000` (`EF_MIPS_ARCH_32`, no ABI bits);
- no `PT_MIPS_ABIFLAGS` record;
- one `PT_LOAD` mapping file offset `0x1000` to virtual address `0x1FC00000`;
- entry point `0x1FC00000`.
That combination hits REA's "recordless legacy" admission. REA records that o32
is an analysis assumption, not a declared ABI fact, and pins Ghidra to
`MIPS:BE:32:default`. The import log confirms:
```
Using Language/Compiler: MIPS:BE:32:default:default
```
## What was collected
The MCP session performed:
- `binary_overview` (segments, counts);
- `list_procedures` (1,074 functions);
- `list_strings` (1,418 defined strings);
- `batch_decompile` of the reset path, main entry and exception handlers;
- `read_function_instructions` for the reset handler and a cache/HPC3 stub;
- `export_evidence_bundle` to `evidence/rea-bundle.json`.
The bundle is a local, git-ignored artifact. `tools/rom-map.py` reads it to
produce the checked-in JSON map and CSV function index.
+494
View File
@@ -0,0 +1,494 @@
{
"source": {
"bytes": 524288,
"sha1": "ac663a4db68528e400487e98cbf901f620fb30ce",
"sha256": "333a0b2e39bb828a28a758b6f633ae7290030129dc747297a1be68dc317b86ae",
"version_string": "SGI Version 5.3 Rev B10 R4X00/R5000 IP24 Feb 12, 1996",
"last_non_ff_offset": 522751,
"last_non_ff_address": "0x1fc7f9ff"
},
"image": {
"base": "0x1fc00000",
"end": "0x1fc80000",
"page_size": 4096
},
"code": {
"start": "0x1fc00000",
"last_function": "0x1fc4a7e0",
"count": 1074
},
"strings": {
"start": "0x1fc01b7c",
"end": "0x1fc7c16b",
"count": 1418
},
"page_kinds": {
"exception-vectors": 1,
"code": 74,
"strings": 17,
"binary-data": 31,
"zero-data": 5
},
"regions": [
{
"kind": "exception-vectors",
"start": "0x1fc00000",
"end": "0x1fc01000",
"procedures": 10,
"strings": 0
},
{
"kind": "code",
"start": "0x1fc01000",
"end": "0x1fc4b000",
"procedures": 1064,
"strings": 103
},
{
"kind": "strings",
"start": "0x1fc4b000",
"end": "0x1fc56000",
"procedures": 0,
"strings": 1272
},
{
"kind": "binary-data",
"start": "0x1fc56000",
"end": "0x1fc6d000",
"procedures": 0,
"strings": 0
},
{
"kind": "strings",
"start": "0x1fc6d000",
"end": "0x1fc6f000",
"procedures": 0,
"strings": 30
},
{
"kind": "zero-data",
"start": "0x1fc6f000",
"end": "0x1fc71000",
"procedures": 0,
"strings": 0
},
{
"kind": "binary-data",
"start": "0x1fc71000",
"end": "0x1fc72000",
"procedures": 0,
"strings": 0
},
{
"kind": "strings",
"start": "0x1fc72000",
"end": "0x1fc74000",
"procedures": 0,
"strings": 2
},
{
"kind": "zero-data",
"start": "0x1fc74000",
"end": "0x1fc76000",
"procedures": 0,
"strings": 0
},
{
"kind": "binary-data",
"start": "0x1fc76000",
"end": "0x1fc78000",
"procedures": 0,
"strings": 0
},
{
"kind": "zero-data",
"start": "0x1fc78000",
"end": "0x1fc79000",
"procedures": 0,
"strings": 0
},
{
"kind": "binary-data",
"start": "0x1fc79000",
"end": "0x1fc7b000",
"procedures": 0,
"strings": 0
},
{
"kind": "strings",
"start": "0x1fc7b000",
"end": "0x1fc7d000",
"procedures": 0,
"strings": 11
},
{
"kind": "binary-data",
"start": "0x1fc7d000",
"end": "0x1fc80000",
"procedures": 0,
"strings": 0
}
],
"exception_vectors": [
{
"offset": 0,
"address": "0x1fc00000",
"word": "0x0bf000f0",
"target": "0x1fc003c0"
},
{
"offset": 16,
"address": "0x1fc00010",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 32,
"address": "0x1fc00020",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 48,
"address": "0x1fc00030",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 64,
"address": "0x1fc00040",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 80,
"address": "0x1fc00050",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 96,
"address": "0x1fc00060",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 112,
"address": "0x1fc00070",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 128,
"address": "0x1fc00080",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 144,
"address": "0x1fc00090",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 160,
"address": "0x1fc000a0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 176,
"address": "0x1fc000b0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 192,
"address": "0x1fc000c0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 208,
"address": "0x1fc000d0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 224,
"address": "0x1fc000e0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 240,
"address": "0x1fc000f0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 256,
"address": "0x1fc00100",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 272,
"address": "0x1fc00110",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 288,
"address": "0x1fc00120",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 304,
"address": "0x1fc00130",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 320,
"address": "0x1fc00140",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 336,
"address": "0x1fc00150",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 352,
"address": "0x1fc00160",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 368,
"address": "0x1fc00170",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 384,
"address": "0x1fc00180",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 400,
"address": "0x1fc00190",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 416,
"address": "0x1fc001a0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 432,
"address": "0x1fc001b0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 448,
"address": "0x1fc001c0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 464,
"address": "0x1fc001d0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 480,
"address": "0x1fc001e0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 496,
"address": "0x1fc001f0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 512,
"address": "0x1fc00200",
"word": "0x0bf003c8",
"target": "0x1fc00f20"
},
{
"offset": 528,
"address": "0x1fc00210",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 544,
"address": "0x1fc00220",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 560,
"address": "0x1fc00230",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 576,
"address": "0x1fc00240",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 592,
"address": "0x1fc00250",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 608,
"address": "0x1fc00260",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 624,
"address": "0x1fc00270",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 640,
"address": "0x1fc00280",
"word": "0x0bf003ca",
"target": "0x1fc00f28"
},
{
"offset": 656,
"address": "0x1fc00290",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 672,
"address": "0x1fc002a0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 688,
"address": "0x1fc002b0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 704,
"address": "0x1fc002c0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 720,
"address": "0x1fc002d0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 736,
"address": "0x1fc002e0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 752,
"address": "0x1fc002f0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 768,
"address": "0x1fc00300",
"word": "0x0bf003c6",
"target": "0x1fc00f18"
},
{
"offset": 784,
"address": "0x1fc00310",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 800,
"address": "0x1fc00320",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 816,
"address": "0x1fc00330",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 832,
"address": "0x1fc00340",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 848,
"address": "0x1fc00350",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 864,
"address": "0x1fc00360",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 880,
"address": "0x1fc00370",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 896,
"address": "0x1fc00380",
"word": "0x0bf00390",
"target": "0x1fc00e40"
},
{
"offset": 912,
"address": "0x1fc00390",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 928,
"address": "0x1fc003a0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
},
{
"offset": 944,
"address": "0x1fc003b0",
"word": "0x0bf001f1",
"target": "0x1fc007c4"
}
]
}
+368
View File
@@ -0,0 +1,368 @@
# Indy IP24 PROM 5.3 Rev B10: structural map
This document maps the newest known SGI Indy boot PROM,
`ip24prom.070-9101-011.bin`. All addresses are PROM addresses as mapped by the
hardware; file offsets are `address - 0x1FC00000`.
Facts in this document come from three places:
1. byte-level inspection of the dump (padding, strings, jump vectors);
2. Ghidra 12.1.2 auto-analysis driven through REA 6.8.0;
3. MAME's IP22/IP24 machine definitions for the surrounding address map.
Function names such as `FUN_1fc02000` are Ghidra auto-generated. The
interpretations attached to them are inferences from their code, call sites and
the strings they reference; they are labelled as such.
## Identity
| Field | Value |
| --- | --- |
| File | `ip24prom.070-9101-011.bin` |
| Size | 524,288 bytes (512 KiB) |
| Part number | 070-9101-011 |
| Version | `SGI Version 5.3 Rev B10 R4X00/R5000 IP24 Feb 12, 1996` |
| SHA-1 | `ac663a4db68528e400487e98cbf901f620fb30ce` |
| SHA-256 | `333a0b2e39bb828a28a758b6f633ae7290030129dc747297a1be68dc317b86ae` |
| MAME set | `indy_4610` |
| Load address | `0x1FC00000` |
| Analyzed functions | 1,074 |
| Analyzed strings | 1,418 |
The SHA-1 matches MAME's `src/mame/sgi/indy_indigo2.cpp` ROM entry exactly.
### The other dumps in the set
The `indy_4610` set contains four PROM revisions. SHA-1 values below are the
ones MAME records.
| File | Version | SHA-1 |
| --- | --- | --- |
| `ip24prom.070-9101-005.bin` | 5.1.2 Rev B4, 9 Dec 1993 | `28b769b28218a1fcd0400dceef9a284dcfbdda5b` |
| `ip24prom.070-9101-007.bin` | 5.0 Rev B6, 28 Sep 1994 | `ade54cd2ecb7064957f8602894f05685e2f4e8fb` |
| `ip24prom.070-9101-008.bin` | 5.3 Rev B7, 16 Feb 1995 | `a752a4aef7e2c6086b8b0244e9f064861a11870f` |
| `ip24prom.070-9101-011.bin` | 5.3 Rev B10, 12 Feb 1996 | `ac663a4db68528e400487e98cbf901f620fb30ce` |
`011` is the newest, and the only one whose version string claims
`R4X00/R5000` support. MAME selects it as the default BIOS for both
`indy_4610` (R4600) and `indy_5015` (R5000).
## How the image was opened
The dump is headerless. REA's raw-binary lane has no MIPS architecture, so a
minimal ELF wrapper is produced by `tools/make-elf-wrapper.py`:
- ELF32, big-endian, `ET_EXEC`, `EM_MIPS`;
- `e_flags = 0x50000000` (`EF_MIPS_ARCH_32`, no ABI field);
- no `PT_MIPS_ABIFLAGS` record;
- one `PT_LOAD` at file offset `0x1000`, virtual address `0x1FC00000`;
- entry `0x1FC00000`.
REA admits this through its recordless legacy MIPS lane and pins Ghidra to
`MIPS:BE:32:default`. The import log confirms that language. The ROM bytes are
not modified.
The declared ABI is therefore absent by construction: o32 calling conventions
are an analysis assumption of the lane, not a property recovered from the ROM.
This affects parameter recovery in decompiled output, not instruction decoding
or control flow.
## Physical placement
MAME's IP24 map places the 512 KiB BIOS window at `0x1FC00000`. Through the MIPS
segments the same bytes are also visible at:
| View | Address |
| --- | --- |
| Physical / KUSEG | `0x1FC00000` |
| KSEG0 (cached) | `0x9FC00000` |
| KSEG1 (uncached) | `0xBFC00000` |
Ghidra analyzed the physical mapping. The reset code itself uses KSEG0/KSEG1
addresses such as `0x9FC7DD18`, `0x9FC4AAC8` and `0xBFA0xxxx`; those appear in
decompiled output as their unmapped KSEG forms.
Relevant neighbours from MAME (`src/mame/sgi/indy_indigo2.cpp`):
| Range | Device |
| --- | --- |
| `0x00000000` - `0x0007FFFF` | Alias for first 512 KiB of RAM |
| `0x08000000` - `0x17FFFFFF` | Low system memory |
| `0x1F000000` - `0x1F3FFFFF` | GIO64 graphics |
| `0x1F400000` - `0x1F5FFFFF` | GIO64 EXP0 |
| `0x1FA00000` - `0x1FA1FFFF` | Memory controller |
| `0x1FB00000` - `0x1FB7FFFF` | HPC3 CHIP1 |
| `0x1FB80000` - `0x1FBFFFFF` | HPC3 CHIP0 |
| `0x1FC00000` - `0x1FFFFFFF` | BIOS (this ROM) |
| `0x20000000` - `0x2FFFFFFF` | High system memory |
| `0x80000000` - `0xFFFFFFFF` | EISA memory |
The reset code touches `0xBFA00xxx`/`0xBFA01xxx` (KSEG1 alias of the memory
controller) and `0xBFBD9xxx`/`0xBFBBxxxx` (KSEG1 alias inside HPC3 CHIP0).
## Image layout
Page-level detail is in [rom-map.json](rom-map.json). The compressed view:
| File offset | PROM address | Size | Content |
| --- | --- | --- | --- |
| `0x00000` - `0x003BF` | `0x1FC00000` - `0x1FC003BF` | 960 B | 60-entry, 16-byte exception jump table |
| `0x003C0` - `0x4A89F` | `0x1FC003C0` - `0x1FC4A89F` | ~298 KiB | Reset handler, code and inline diagnostic strings; 1,074 functions, last one starting at `0x1FC4A7E0` |
| `0x4A8A0` - `0x5568F` | `0x1FC4A8A0` - `0x1FC5568F` | ~44 KiB | Core message blob: boot sequence, diagnostics, monitor help, environment and ARCS strings |
| `0x56000` - `0x6CFFF` | `0x1FC56000` - `0x1FC6CFFF` | 92 KiB | Binary tables; no Ghidra-defined strings |
| `0x6D000` - `0x6EA4C` | `0x1FC6D000` - `0x1FC6EA4C` | ~6.5 KiB | Path-test strings and the environment variable/default table |
| `0x6EA4D` - `0x7BF8F` | `0x1FC6EA4D` - `0x1FC7BF8F` | ~54 KiB | Mostly zero/binary pages plus two isolated strings |
| `0x7BF90` - `0x7C16B` | `0x1FC7BF90` - `0x1FC7C16B` | ~480 B | SCSI tape drive name table |
| `0x7C16C` - `0x7F9FF` | `0x1FC7C16C` - `0x1FC7F9FF` | ~14 KiB | Remaining data tables |
| `0x7FA00` - `0x7FFFF` | `0x1FC7FA00` - `0x1FC7FFFF` | 1536 B | `0xFF` padding |
Only four `0xFF` runs of 16 bytes or more exist; three are tiny alignment holes
inside the string area, and the real padding is the final 1,536 bytes. The last
non-`0xFF` byte is at file offset `0x7F9FF`.
The code and message regions overlap at the page level: page
`0x1FC4A000`-`0x1FC4AFFF` holds the last nine functions and 64 strings, so the
"code ends" boundary above is the end of executable content, not the start of
all strings.
## Exception vector table
The first 960 bytes are 60 cells of 16 bytes:
```
0x1FC00000 + (index * 0x10)
```
Each cell begins with an absolute MIPS `J` instruction followed by three
`NOP`s. The reset handler lives at `0x1FC003C0`, immediately after the table, so
the final four 16-byte slots are not table entries.
Observed targets:
| Cell address | Index | Target | Count |
| --- | --- | --- | --- |
| `0x1FC00000` | 0 | `0x1FC003C0` (reset handler) | 1 |
| `0x1FC00200` | 32 | `0x1FC00F20` | 1 |
| `0x1FC00280` | 40 | `0x1FC00F28` | 1 |
| `0x1FC00300` | 48 | `0x1FC00F18` | 1 |
| `0x1FC00380` | 56 | `0x1FC00E40` | 1 |
| all other cells | - | `0x1FC007C4` (shared handler) | 55 |
The architectural MIPS R4000 vector offsets (`0x080` TLB refill, `0x100` general
exception, `0x180` XTLB refill, `0x200` cache error) fall inside this table. Of
those, only `0x200` is special; the rest use the shared handler. The meaning of
indices 32, 40, 48 and 56 is not established by this analysis. One plausible
reading is an SGI dispatch table indexed by a cause-derived value rather than by
the architectural vector offset, but that is an inference, not an observation.
The four special targets all land inside the block at `0x1FC00E40`-`0x1FC00F28`.
Ghidra did not split that block into separate functions; auto-analysis treats
`0x1FC00DCC` as the nearest function start. The bytes there are worth a focused
follow-up with manual function creation.
## Reset and boot flow
### Reset handler, `0x1FC003C0`
Observed instruction sequence (REA/Ghidra):
1. `lui v0, 0x3041; mtc0 v0, Status` and clear `Cause`, `PageMask`,
`WatchLo`, `WatchHi`.
2. `Config = 2`, then test `Config` bit 15. If clear, call `FUN_1fc0a810`,
which reads and rewrites a CP0 configuration value and writes
`0xBFA00000 = 0x10000`.
3. Write memory-controller registers at `0xBFA000EC`, `0xBFA000FC`,
`0xBFA00004`, `0xBFA0000C`, `0xBFA00084`, `0xBFA000D4`,
`0xBFA000DC` and `0xBFA0002C`. The value stored at `0xBFA00084` is loaded
from `0x9FC7DD18`.
4. Poll `0xBFA01004` until the counter advances by at least `0x271`.
5. Call `0x1FC00A3C`, `0x1FC00BD0`, `0x1FC00DCC`, `0x1FC00D58`.
6. Set `s8 = 0x9FC03660`, then call `0x1FC039E0`, `0x1FC03DE0`,
`0x1FC03EB0`, `0x1FC01070`.
7. Sample `0xBFA000C4`, test bits `0x2200`, optionally set low bits of
`0xBFA00004`.
8. Call `0x1FC009D4`, then `j 0x1FC02000`.
The Ghidra decompiler renders the end of this function as an infinite loop
because it does not model the final `j` as a return. The instruction listing is
the authority here.
### Cold-start main, `0x1FC02000`
The function reached by the reset jump initializes HPC3/IOC registers around
`0xBFBD9xxx`, calls a long sequence of hardware setup routines, and then reads
the `diagmode` environment variable at `0x9FC4AAC8`.
Two paths follow:
- diagnostics/menu path (`FUN_1fc02a50`) when the diagnostic state is set;
- autoboot path (`FUN_1fc0fb10`) otherwise.
`FUN_1fc0fb10` reads `AutoLoad` (`0x1FC4D090`) and `NoAutoLoad`
(`0x1FC4D09C`). If autoload is enabled it prints the maintenance prompt
`To perform system maintenance instead, press <Esc>` and attempts a boot; a
failure prints `Unable to boot; press any key to continue:` and falls back
through `FUN_1fc00624`.
`FUN_1fc02a50` is the menu/boot loop. It reads `VERBOSE` and `diskless`,
touches IOC interrupt-enable bits, and loops through the maintenance menu whose
strings sit at `0x1FC4D99C`:
- `Install System Software`
- `Recover System`
- `Enter Command Monitor`
- `Select Keyboard Layout`
The same area contains `Autoboot failed`, `Diagnostics` and
`Command Monitor. Type "exit" %sto return to the menu.`
### Boot-sequence messages
The ordered diagnostic strings beginning at `0x1FC4A8A0` describe the intended
power-on sequence:
1. `Power switch released.`
2. `Initializing primary/secondary caches.`
3. `Secondary cache size is 0x%x (%d Kbytes)`
4. `Initializing exception handlers.`
5. `Initializing Status Register.`
6. `Enabling PLP, ISDN B/A DMA channels.`
7. `Selecting Gen Control register bits.`
8. `Initialize Map Polarity.`
9. `Deasserting VIDEO Reset.`
10. `Playing startup tune.`
11. `Initializing environment.`
12. `Executing remainder of diagnostic.`
13. `Initializing software and devices.`
14. `All initialization and diagnostic completed.`
## Function anchors
| Address | Auto name | Interpretation | Basis |
| --- | --- | --- | --- |
| `0x1FC00000` | `entry` | Table slot 0 thunk to the reset handler | Ghidra symbol, thunk target |
| `0x1FC003C0` | `FUN_1fc003c0` | Reset handler | CP0 init, memory-controller setup, jump to main |
| `0x1FC00624` | `FUN_1fc00624` | Shared exception/fallback path | Vector-table default target; cache and memory scrub code |
| `0x1FC00DCC` | `FUN_1fc00dcc` | Short HPC3/cache stub | Writes `0xBFB91004`, `0xBFBC0003`, `0xBFBC0007`; nearest function for special vectors |
| `0x1FC02000` | `FUN_1fc02000` | Cold-start main | Reached by reset jump; HPC3/IOC init; branches to diagnostics or autoboot |
| `0x1FC02A50` | `FUN_1fc02a50` | Menu/boot loop | Reads `VERBOSE`/`diskless`; loops over maintenance menu |
| `0x1FC0FB10` | `FUN_1fc0fb10` | Autoboot policy | Reads `AutoLoad`/`NoAutoLoad`; maintenance prompt and boot failure text |
The full list of 1,074 function starts is in
[procedure-index.csv](procedure-index.csv). The CSV has three columns:
`address,name,thunk_target`.
## Firmware feature map
### Command monitor
Help strings at `0x1FC4ACE8`-`0x1FC4B054` enumerate the monitor commands:
`autoboot` (`auto`), `boot`, `checksum`, `date`, `dump`, `exit`, `fill`, `get`,
`go`, `help`/`?`, `initialize` (`init`), `inventory`, `passwd`, `printenv`,
`put`, `reboot`, `resetenv`, `resetpw`, `setenv`, `single`, `unsetenv`,
`version`.
The banner is `PROM Monitor %s (BE)` at `0x1FC4B090`, followed by the version
string.
### Boot targets
Strings at `0x1FC4AAF0`-`0x1FC4AB44` name the default load paths:
- `bootp()ide.IP24`
- `sashIP24`
- `bootp()sashIP24`
- `bootp()unix.IP24`
Elsewhere the image references `sashARCS`, `part(8)sashARCS`, `/stand/ide`,
IDE distribution CD boot, and ARCS-style boot.
### Diagnostics
Diagnostic result strings name the tests the PROM can run:
- memory probe, memory data test, memory address test, walking-bit test;
- cache diagnostic;
- graphics diagnostic;
- interrupt controller diagnostic;
- SCSI controller, SCSI device/cable, WD SCSI0 path test;
- RTC path test, INT path test, data path test;
- PC keyboard/mouse controller, keyboard, mouse;
- CPU base board, graphics board, CPU module, SIMM replacement prompts.
The display-side strings also expose the VDMA and DMA error registers
(`DMA_CAUSE`, `DMA_MEMADR`, `CPU_MEMACC`, `GIO_MEMACC`, `CPU_ERR_ADDR`,
`CPU_ERR_STAT`, `GIO_ERR_ADDR`, `GIO_ERR_STAT`) and the MIPS exception dump
fields (`ErrEPC`, `BadVaddr`, `Cause`, `Status`, `CacheErr`, `CpuParityErr`,
`GioParityErr`, `LIOstatus0..2`, `CpuErrorAddr`, `GioErrorAddr`).
### Environment variables
The environment table around `0x1FC6E4F0`-`0x1FC6EA4C` and the help strings
around `0x1FC4CD00`-`0x1FC4E000` cover:
`AutoLoad`, `NoAutoLoad`, `diagmode`, `console`, `diskless`, `VERBOSE`,
`eaddr`, `netaddr`, `srvaddr`, `gateaddr`, `netinsthost`, `netinstfile`,
`tapedevice`, `scsihostid`, `scsiretries`, `pagecolor`, `TimeZone`,
`autopower`, `SystemPartition`, `OSLoader`, `OSLoadPartition`,
`OSLoadFilename`/`OSLoadFileName`, `OSLoadOptions`, `ConsoleIn`,
`ConsoleOut`, `ConsoleWarning`, `passwd_key`, `netpasswd_key`.
The NVRAM path has its own messages: `NVRAM checksum is incorrect:
reinitializing.`, `nvchecksum:`, `Checksum mismatch: is 0x%x, s/b 0x%x`,
`Enter new password:`, `Confirm new password:`, `Password cleared.` and the
password-jumper warnings.
### ARCS and SCSI tables
The string blob around `0x1FC5549C`-`0x1FC55524` contains the ARCS component
names `ExceptionBlock`, `SystemParameterBlock`, `FreeContiguous`, `FreeMemory`,
`BadMemory`, `LoadedProgram`, `FirmwareTemporary` and `FirmwarePermanent`.
The cluster at `0x1FC7BF90`-`0x1FC7C16B` is a SCSI tape-drive name table:
`CIPHER`, `TANDBERG TDC 3660`, `ARCHIVE VIPER 150`, `VIPER 60`, `Python`,
`EXABYTE` and `KENNEDY`.
The sentinel value `cafebabedeadbeef` appears at `0x1FC6EA4C`; it is likely an
uninitialised-memory pattern used by the environment/NVRAM code, but that is an
inference.
## Confidence and limitations
- Function boundaries and names come from Ghidra auto-analysis. There are no
symbols in the image beyond the imported `entry` label.
- The ELF wrapper declares no ABI. o32 is an analysis assumption, so recovered
parameter counts and types are weaker than the control flow.
- The vector table's 60 x 16-byte structure and observed targets are facts.
What each index means is not established.
- The decompiler output is heuristic. Where it conflicts with the instruction
listing (for example the reset handler's final jump), the instruction listing
is authoritative.
- String inventory covers Ghidra-defined string data only (1,418 entries).
Undefined or packed text may be missing, and an absent string is not proof
that the text is absent from the artifact.
- No code was executed. Nothing here establishes runtime behaviour.
- Ghidra 12.1.2 was used; REA's bridge is verified against 12.1.4 and accepts
other 12.1.x builds.
## Reproduce
```bash
./tools/fetch-roms.sh
./tools/make-elf-wrapper.py \
roms/ip24prom.070-9101-011.bin -o roms/indy-ip24-5.3-b10.elf
export GHIDRA_INSTALL_DIR=/opt/ghidra
rea analyze roms/indy-ip24-5.3-b10.elf --provider ghidra --json
```
The checked-in artifacts were produced with the REA MCP session described in
[rea-setup.md](rea-setup.md), then:
```bash
./tools/rom-map.py roms/ip24prom.070-9101-011.bin \
--evidence evidence/rea-bundle.json \
--output docs/rom-map.json \
--procedures-csv docs/procedure-index.csv
```
+15
View File
@@ -0,0 +1,15 @@
# ROM dumps
The binaries themselves are not committed; they are copyrighted SGI firmware.
Run `../tools/fetch-roms.sh` to download the MAME 0.236 `indy_4610` set and
verify it against `SHA1SUMS`.
| File | Version | Notes |
| --- | --- | --- |
| `ip24prom.070-9101-005.bin` | 5.1.2 Rev B4, 1993-12-09 | Oldest dump in the set |
| `ip24prom.070-9101-007.bin` | 5.0 Rev B6, 1994-09-28 | |
| `ip24prom.070-9101-008.bin` | 5.3 Rev B7, 1995-02-16 | |
| `ip24prom.070-9101-011.bin` | 5.3 Rev B10, 1996-02-12 | Newest; `.elf` wrapper used for analysis |
`indy-ip24-5.3-b10.elf` is generated locally by
`../tools/make-elf-wrapper.py` and is also ignored.
+4
View File
@@ -0,0 +1,4 @@
ac663a4db68528e400487e98cbf901f620fb30ce ip24prom.070-9101-011.bin
a752a4aef7e2c6086b8b0244e9f064861a11870f ip24prom.070-9101-008.bin
ade54cd2ecb7064957f8602894f05685e2f4e8fb ip24prom.070-9101-007.bin
28b769b28218a1fcd0400dceef9a284dcfbdda5b ip24prom.070-9101-005.bin
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Fetch the MAME 0.236 SGI Indy (IP24) PROM set and verify every dump.
#
# The four images are the complete ROM set for MAME's indy_4610 machine.
# SHA-1 values come from MAME's src/mame/sgi/indy_indigo2.cpp ROM definitions.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEST="${ROOT}/roms"
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT
URL="https://archive.org/download/mame-0.236-roms-split/MAME%200.236%20ROMs%20%28split%29/indy_4610.zip"
mkdir -p "${DEST}"
echo "Downloading ${URL}"
curl -fSL --retry 3 -o "${WORK}/indy_4610.zip" "${URL}"
unzip -q -o "${WORK}/indy_4610.zip" -d "${DEST}"
declare -A SHA1=(
[ip24prom.070-9101-005.bin]=28b769b28218a1fcd0400dceef9a284dcfbdda5b
[ip24prom.070-9101-007.bin]=ade54cd2ecb7064957f8602894f05685e2f4e8fb
[ip24prom.070-9101-008.bin]=a752a4aef7e2c6086b8b0244e9f064861a11870f
[ip24prom.070-9101-011.bin]=ac663a4db68528e400487e98cbf901f620fb30ce
)
status=0
for name in "${!SHA1[@]}"; do
got="$(sha1sum "${DEST}/${name}" | cut -d' ' -f1)"
if [[ "${got}" == "${SHA1[${name}]}" ]]; then
printf 'ok %s %s\n' "${got}" "${name}"
else
printf 'FAIL %s %s (expected %s)\n' "${got}" "${name}" "${SHA1[${name}]}" >&2
status=1
fi
done
exit "${status}"
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Wrap a raw SGI Indy IP24 PROM dump in a minimal ELF32 big-endian MIPS image.
REA's Ghidra MIPS lane accepts ELF32 ET_EXEC images, but not headerless MIPS
binaries. This tool produces a load-image-only wrapper so the PROM can be
opened at its real physical address (0x1FC00000) without modifying the ROM
bytes.
The wrapper deliberately declares EF_MIPS_ARCH_32 with an unspecified ABI and
provides no PT_MIPS_ABIFLAGS record. That selects REA's "recordless legacy"
admission, which pins Ghidra to MIPS:BE:32:default, a suitable match for the
R4000/R4600-era Indy PROM.
"""
from __future__ import annotations
import argparse
import os
import struct
import sys
ROM_BASE = 0x1FC00000
PAGE_SIZE = 0x1000
EM_MIPS = 8
ET_EXEC = 2
EF_MIPS_ARCH_32 = 0x50000000
EI_NIDENT = 16
def build_elf(rom: bytes, base: int, entry: int) -> bytes:
payload_offset = PAGE_SIZE
e_ident = bytearray(EI_NIDENT)
e_ident[0:4] = b"\x7fELF"
e_ident[4] = 1 # ELFCLASS32
e_ident[5] = 2 # ELFDATA2MSB
e_ident[6] = 1 # EV_CURRENT
e_ident[7] = 0 # ELFOSABI_NONE
e_entry = entry
e_phoff = 52
e_shoff = 0
e_flags = EF_MIPS_ARCH_32
e_ehsize = 52
e_phentsize = 32
e_phnum = 1
e_shentsize = 0
e_shnum = 0
e_shstrndx = 0
ehdr = struct.pack(
">16sHHIIIIIHHHHHH",
bytes(e_ident),
ET_EXEC,
EM_MIPS,
1,
e_entry,
e_phoff,
e_shoff,
e_flags,
e_ehsize,
e_phentsize,
e_phnum,
e_shentsize,
e_shnum,
e_shstrndx,
)
# PT_LOAD: file offset 0x1000 -> virtual address base, whole ROM.
phdr = struct.pack(
">IIIIIIII",
1, # PT_LOAD
payload_offset,
base,
base,
len(rom),
len(rom),
5, # PF_R | PF_X
PAGE_SIZE,
)
header = ehdr + phdr
assert len(header) <= payload_offset
return header.ljust(payload_offset, b"\x00") + rom
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("rom", help="raw PROM binary (512 KiB)")
parser.add_argument("-o", "--output", help="output ELF path")
parser.add_argument("--base", type=lambda s: int(s, 0), default=ROM_BASE)
parser.add_argument("--entry", type=lambda s: int(s, 0), default=None)
args = parser.parse_args()
with open(args.rom, "rb") as fh:
rom = fh.read()
if len(rom) != 0x80000:
print(
f"warning: {args.rom} is {len(rom)} bytes, expected 524288", file=sys.stderr
)
entry = args.base if args.entry is None else args.entry
out = args.output
if out is None:
stem, _ = os.path.splitext(args.rom)
out = stem + ".elf"
with open(out, "wb") as fh:
fh.write(build_elf(rom, args.base, entry))
print(out)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Build a structural map of an SGI Indy IP24 PROM image.
Combines byte-level layout facts (padding, printable density, MIPS jump
vectors) with procedure and string inventories exported from a REA session.
Usage:
tools/rom-map.py roms/ip24prom.070-9101-011.bin \
--evidence evidence/rea-bundle.json \
--output docs/rom-map.json
"""
from __future__ import annotations
import argparse
import hashlib
import json
import struct
from collections import Counter
from pathlib import Path
BASE = 0x1FC00000
PAGE = 0x1000
VECTOR_STRIDE = 0x10
# 60 entries of 16 bytes. The reset handler itself starts at 0x1FC003C0 and
# overwrites what would otherwise be the final four entries.
VECTOR_TABLE_SIZE = 0x3C0
def sha1(data: bytes) -> str:
return hashlib.sha1(data).hexdigest()
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def printable_ratio(chunk: bytes) -> float:
if not chunk:
return 0.0
printable = sum(
1 for b in chunk if 0x20 <= b <= 0x7E or b in (0x09, 0x0A, 0x0D)
)
return round(printable / len(chunk), 4)
def zero_ratio(chunk: bytes) -> float:
return round(chunk.count(0) / len(chunk), 4) if chunk else 0.0
def ff_ratio(chunk: bytes) -> float:
return round(chunk.count(0xFF) / len(chunk), 4) if chunk else 0.0
def j_target(word: int, pc: int) -> int | None:
"""Decode a MIPS J/JAL absolute target, or None for other opcodes."""
if (word >> 26) not in (0x02, 0x03):
return None
return ((pc + 4) & 0xF0000000) | ((word & 0x03FFFFFF) << 2)
def vector_table(rom: bytes) -> list[dict]:
rows = []
for off in range(0, min(VECTOR_TABLE_SIZE, len(rom)), VECTOR_STRIDE):
word = struct.unpack(">I", rom[off : off + 4])[0]
target = j_target(word, BASE + off)
rows.append(
{
"offset": off,
"address": f"0x{BASE + off:08x}",
"word": f"0x{word:08x}",
"target": f"0x{target:08x}" if target is not None else None,
}
)
return rows
def load_inventory(path: Path | None) -> tuple[list[dict], list[dict]]:
if path is None or not path.exists():
return [], []
bundle = json.loads(path.read_text())
procedures: list[dict] = []
strings: list[dict] = []
for record in bundle.get("records", []):
operation = record.get("operation")
if operation == "list_procedures":
procedures = record.get("normalized_result", [])
elif operation == "list_strings":
strings = record.get("normalized_result", [])
return procedures, strings
def classify(page: dict, last_non_ff: int) -> str:
if page["offset"] >= last_non_ff or page["ff_ratio"] > 0.9:
return "padding"
if page["address"] < BASE + VECTOR_TABLE_SIZE:
return "exception-vectors"
if page["procedures"]:
return "code"
if page["strings"]:
return "strings"
if page["printable_ratio"] > 0.6:
return "text-data"
if page["zero_ratio"] > 0.5:
return "zero-data"
return "binary-data"
def build(rom: bytes, procedures: list[dict], strings: list[dict]) -> dict:
proc_addrs = sorted(int(p["address"], 16) for p in procedures)
str_addrs = sorted(int(s["address"], 16) for s in strings)
last_non_ff = max((i for i, b in enumerate(rom) if b != 0xFF), default=-1)
pages = []
for off in range(0, len(rom), PAGE):
chunk = rom[off : off + PAGE]
addr = BASE + off
page = {
"offset": off,
"address": addr,
"procedures": sum(1 for a in proc_addrs if addr <= a < addr + PAGE),
"strings": sum(1 for a in str_addrs if addr <= a < addr + PAGE),
"printable_ratio": printable_ratio(chunk),
"zero_ratio": zero_ratio(chunk),
"ff_ratio": ff_ratio(chunk),
}
page["kind"] = classify(page, last_non_ff)
pages.append(page)
# Collapse equal neighbouring classifications into ranges.
regions = []
for page in pages:
if regions and regions[-1]["kind"] == page["kind"]:
regions[-1]["end"] = page["address"] + PAGE
regions[-1]["procedures"] += page["procedures"]
regions[-1]["strings"] += page["strings"]
else:
regions.append(
{
"kind": page["kind"],
"start": page["address"],
"end": page["address"] + PAGE,
"procedures": page["procedures"],
"strings": page["strings"],
}
)
version = next(
(s["value"] for s in strings if s["value"].startswith("SGI Version")), None
)
code_span = None
if proc_addrs:
code_span = {
"start": f"0x{min(proc_addrs):08x}",
"last_function": f"0x{max(proc_addrs):08x}",
"count": len(proc_addrs),
}
string_span = None
if str_addrs:
string_span = {
"start": f"0x{min(str_addrs):08x}",
"end": f"0x{max(str_addrs):08x}",
"count": len(str_addrs),
}
histogram = Counter(p["kind"] for p in pages)
return {
"source": {
"bytes": len(rom),
"sha1": sha1(rom),
"sha256": sha256(rom),
"version_string": version,
"last_non_ff_offset": last_non_ff,
"last_non_ff_address": (
f"0x{BASE + last_non_ff:08x}" if last_non_ff >= 0 else None
),
},
"image": {
"base": f"0x{BASE:08x}",
"end": f"0x{BASE + len(rom):08x}",
"page_size": PAGE,
},
"code": code_span,
"strings": string_span,
"page_kinds": dict(histogram),
"regions": [
{
**r,
"start": f"0x{r['start']:08x}",
"end": f"0x{r['end']:08x}",
}
for r in regions
],
"exception_vectors": vector_table(rom),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("rom")
parser.add_argument("--evidence", type=Path, default=None)
parser.add_argument("--output", type=Path, default=None)
parser.add_argument(
"--procedures-csv",
type=Path,
default=None,
help="write the analysed function start addresses as CSV",
)
args = parser.parse_args()
rom = Path(args.rom).read_bytes()
procedures, strings = load_inventory(args.evidence)
doc = build(rom, procedures, strings)
if args.procedures_csv and procedures:
args.procedures_csv.parent.mkdir(parents=True, exist_ok=True)
rows = ["address,name,thunk_target"]
for proc in sorted(procedures, key=lambda p: int(p["address"], 16)):
rows.append(
"{address},{value},{target}".format(
address=proc["address"],
value=proc["value"],
target=proc.get("procedure", {}).get("thunk_target") or "",
)
)
args.procedures_csv.write_text("\n".join(rows) + "\n")
print(args.procedures_csv)
text = json.dumps(doc, indent=2)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text + "\n")
print(args.output)
else:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())