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:
commit
1dc99443b7
11 files changed
+2562
No files matched your search
+17
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Executable
+39
@@ -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}"
|
||||
Executable
+115
@@ -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())
|
||||
Executable
+245
@@ -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())
|
||||
Reference in new issue
Block a user