mirror of
https://github.com/radareorg/r2hexpat.git
synced 2026-11-08 17:42:41 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a5fbfc7f | ||
|
|
d7635ca49e |
No files matched your search
@@ -24,13 +24,13 @@ all: $(OUT)
|
||||
$(OUT): r2/plugin.ts r2/r2.d.ts $(LIB)
|
||||
$(BUNDLE) r2/plugin.ts
|
||||
|
||||
tests/unit.js: tests/unit.ts tests/search_unit.ts tests/magic_unit.ts $(LIB)
|
||||
tests/unit.js: tests/unit.ts tests/search_unit.ts tests/magic_unit.ts tests/tree_unit.ts $(LIB)
|
||||
$(BUNDLE) tests/unit.ts
|
||||
|
||||
tests/testlib.js: tests/testlib.ts tests/upstream_hooks.ts $(LIB)
|
||||
$(BUNDLE) tests/testlib.ts
|
||||
|
||||
tests/upstream.r2.js: tests/upstream_r2.ts tests/upstream_hooks.ts tests/search_unit.ts tests/magic_unit.ts $(LIB)
|
||||
tests/upstream.r2.js: tests/upstream_r2.ts tests/upstream_hooks.ts tests/search_unit.ts tests/magic_unit.ts tests/tree_unit.ts $(LIB)
|
||||
$(BUNDLE) tests/upstream_r2.ts
|
||||
|
||||
user-install: $(OUT)
|
||||
|
||||
@@ -31,11 +31,13 @@ Commands (all output goes through r2's console, so `~grep` and `|` work):
|
||||
| `hexpat- [n]` | unload all loaded files (or the nth) |
|
||||
| `hexpat-v` | show the r2hexpat version |
|
||||
| `hexpatj [file]` | patterns as JSON (`hexpatj~{}` to indent) |
|
||||
| `hexpat* [file]` | patterns as r2 commands: flags in the `hexpat` flagspace, `Cd`/`Cs` data hints and value comments (`.hexpat*` applies them) |
|
||||
| `hexpat* [file]` | patterns as r2 commands: flags in the `hexpat` flagspace, flag colors/comments, `Cd`/`Cs` data hints and value comments (`.hexpat*` applies them) |
|
||||
| `hexpat-*` | r2 commands to remove generated flags (`.hexpat-*` applies them) |
|
||||
| `hexpatl[j]` | list loaded files and their top-level patterns |
|
||||
| `hexpate expr` | evaluate an expression or assignment in the last loaded file (`hexpate sizeof(header)`, `hexpate header.value = 3`) |
|
||||
| `hexpatp[?+=-ijl] ...` | build partial instances and masked search patterns (`hexpatp?` for help) |
|
||||
| `hexpatm [options] Type` | print a libmagic header decoder (`hexpatm?` for help) |
|
||||
| `hexpatt[j*] [options] [Type [address]\|pattern]` | view a type at an address or a loaded pattern as a tree, JSON or r2 commands (`hexpatt?` for help) |
|
||||
| `hexpats [file]` | one-line summary (types with their field count, enums, functions, patterns, size) or the error, meant for status bars |
|
||||
| `hexpat?` | help |
|
||||
|
||||
@@ -57,6 +59,46 @@ the transform has no inverse.
|
||||
Quote the whole r2 command when it contains operators that r2 handles itself,
|
||||
for example `"hexpate header.value |= 8"` or `"hexpate header.value >= 3"`.
|
||||
|
||||
## Address-bound trees and flag colors
|
||||
|
||||
`hexpatt` displays a type's fields as a tree with addresses, values, sizes
|
||||
and evaluated attributes, including `color`, `comment` and `name`:
|
||||
|
||||
```text
|
||||
hexpatt -f types.hexpat Header @ 0x100
|
||||
hexpat -q types.hexpat
|
||||
hexpatt Header 0x100
|
||||
hexpatt header
|
||||
hexpatt header.inner
|
||||
hexpatt
|
||||
```
|
||||
|
||||
A type uses the current r2 seek when no address is given; an explicit address
|
||||
can be an r2 expression or flag name. `-f file` supplies types without loading
|
||||
the file's placements, executing its top-level statements or calling `main()`.
|
||||
It accepts `base64:` sources and `-I dir` for includes. Without `-f`, types come
|
||||
from the last loaded file. Type decoding follows ordinary hexpat declarations,
|
||||
including data-dependent arrays and pointers, and honors the binary base
|
||||
address or `#pragma base_address`. These one-shot views preserve loaded files
|
||||
and search instances. A loaded pattern name or nested path shows its existing
|
||||
tree; no arguments shows all loaded patterns.
|
||||
|
||||
Rows display their color attributes even with `e scr.color=0`; enabling colors
|
||||
also colors the rows. Enclosing aggregate colors apply to members that have no
|
||||
explicit color of their own. `hexpattj` and `hexpatj` expose `color`, `comment`,
|
||||
`displayName` and evaluated `attributes` in JSON.
|
||||
|
||||
`hexpat*` and `hexpatt*` emit `fc flag=rgb:RRGGBB` for colored flags and `fC`
|
||||
for pattern comments. The runtime's `std::core::set_pattern_color` is supported
|
||||
too. Apply a type's flags directly with `.hexpatt* -f types.hexpat Header @ 0x100`.
|
||||
|
||||
`hexpat-*` prints a removal script; `.hexpat-*` applies it. This removes flags
|
||||
matching `hexpat.*` in the `hexpat` flagspace, including their flag metadata,
|
||||
and restores the previous flagspace. It works after the patterns are unloaded
|
||||
and preserves other flags, loaded files and search instances. Address comments
|
||||
and data hints remain. `hexpat-` (or `hexpat- *`, with a space) still unloads
|
||||
the pattern files.
|
||||
|
||||
## Search patterns
|
||||
|
||||
`hexpatp` instantiates a type without a backing address. All fields start
|
||||
@@ -189,6 +231,12 @@ evaluates an expression or assignment in the global scope of an evaluated patter
|
||||
functions, types, pragma handlers and `in` variables, like the C++ runtime's
|
||||
`addFunction`/`addType`/`addPragma`.
|
||||
|
||||
`decodeType(program, "Header", host, offset)` decodes one type at a host-relative
|
||||
offset after preparing types and constants only. Its root pattern is named
|
||||
`value` (with underscores added if a constant already uses that name).
|
||||
`dumpTree(inst, print, { base, color, maxEntries })` renders a tree
|
||||
with metadata; `patterns` can select specific roots.
|
||||
|
||||
Hosts can implement `write(addr, bytes)` for interactive field assignments;
|
||||
read-only hosts reject those writes. Assignments while evaluating a pattern
|
||||
file retain ImHex's value overrides and do not write host data.
|
||||
|
||||
+30
-10
@@ -158,6 +158,24 @@ export class PatternInstance {
|
||||
this.execBlock(constants(this.program.body), this.global);
|
||||
}
|
||||
|
||||
/** Decode a type against the input at an offset, without running top-level code. */
|
||||
placeType(type: string, offset = 0, name?: string): Pattern {
|
||||
if (!this.global) this.prepareTypes();
|
||||
if (!Number.isSafeInteger(offset) || offset < 0 || offset > this.data.size())
|
||||
this.error("type offset is outside the data");
|
||||
this.frames = [this.global];
|
||||
this.cursor = offset;
|
||||
if (name === undefined) {
|
||||
name = "value";
|
||||
while (this.global.scope.vars.has(name)) name += "_";
|
||||
}
|
||||
const p = this.create(this.resolveType(parseType(type, this.types.keys())), name);
|
||||
this.declare(name, { v: p }, this.global.scope, this.loc);
|
||||
this.patterns.push(p);
|
||||
this.variables.set(name, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Build a fixed layout at offset zero, without reading any instance data. */
|
||||
instantiateType(type: string, options: LayoutOptions = {}): Pattern {
|
||||
const alignment = options.alignment === undefined ? 1 : options.alignment;
|
||||
@@ -1836,19 +1854,14 @@ export class PatternInstance {
|
||||
|
||||
private dumpPattern(p: Pattern, indent: number, print: (s: string) => void, name: string): void {
|
||||
if (p.hidden) return;
|
||||
if (p.displayName) name = p.displayName;
|
||||
const pad = "".padStart(indent * 2, " ");
|
||||
const addr = p.local ? "local" : "0x" + (this.dumpBase + p.offset).toString(16).padStart(8, "0");
|
||||
const type = p.kind === "bitfield_field" ? p.typeName + ":" + p.bits : p.typeName;
|
||||
let line = pad + name + " (" + type + ") @ " + addr;
|
||||
print(pad + this.describePattern(p, this.dumpBase, name));
|
||||
if (p.displayName) name = p.displayName;
|
||||
if (p.kind === "pointer") {
|
||||
const v = this.value(p) as bigint;
|
||||
print(line + " = " + fmtInt(v));
|
||||
if (p.pointee) this.dumpPattern(p.pointee, indent + 1, print, p.pointee.name || "*" + name);
|
||||
return;
|
||||
}
|
||||
if (p.isComposite() && !p.formatFn && !p.transformFn) {
|
||||
print(line + " size=" + p.size);
|
||||
if (p.sealed) return;
|
||||
if (p.kind === "array") {
|
||||
const n = p.entryCount();
|
||||
@@ -1856,9 +1869,16 @@ export class PatternInstance {
|
||||
} else for (const c of p.children || []) this.dumpPattern(c, indent + 1, print, c.name);
|
||||
return;
|
||||
}
|
||||
if (p.kind === "padding") { print(line + " size=" + p.size); return; }
|
||||
line += " = " + this.dumpValue(p);
|
||||
print(line);
|
||||
}
|
||||
|
||||
/** One pattern row, shared by the indented dump and the metadata tree. */
|
||||
describePattern(p: Pattern, base = 0, name = p.name): string {
|
||||
const addr = p.local ? "local" : "0x" + (base + p.offset).toString(16).padStart(8, "0");
|
||||
const type = p.kind === "bitfield_field" ? p.typeName + ":" + p.bits : p.typeName;
|
||||
const line = (p.displayName || name) + " (" + type + ") @ " + addr;
|
||||
if (p.kind === "pointer") return line + " = " + fmtInt(this.value(p) as bigint);
|
||||
if ((p.isComposite() && !p.formatFn && !p.transformFn) || p.kind === "padding") return line + " size=" + p.size;
|
||||
return line + " = " + this.dumpValue(p);
|
||||
}
|
||||
|
||||
private dumpValue(p: Pattern): string {
|
||||
|
||||
+9
-2
@@ -12,8 +12,8 @@ import * as A from "./ast";
|
||||
export { parse, PatternInstance, PatternError, BufferHost };
|
||||
export type { HexpatHost, HostHooks };
|
||||
export { Pattern } from "./patterns";
|
||||
export { toJson, patternToJson, walkPatterns, jsonValue } from "./output";
|
||||
export type { PatternJson, JsonOptions } from "./output";
|
||||
export { toJson, patternToJson, walkPatterns, jsonValue, dumpTree, patternColor } from "./output";
|
||||
export type { PatternJson, JsonOptions, TreeOptions } from "./output";
|
||||
export { SearchPattern } from "./search";
|
||||
export type { SearchOptions, SearchField, SearchEncoding, FieldState } from "./search";
|
||||
|
||||
@@ -35,6 +35,13 @@ export function evaluate(program: A.Program, host: HexpatHost, hooks?: HostHooks
|
||||
return inst;
|
||||
}
|
||||
|
||||
/** Decode one type at a host offset, preparing types and constants only. */
|
||||
export function decodeType(program: A.Program, type: string, host: HexpatHost, offset = 0, hooks?: HostHooks): PatternInstance {
|
||||
const inst = new PatternInstance(program, new HostMemory(host), hooks, host.print ? (s) => host.print!(s) : undefined);
|
||||
inst.placeType(type, offset);
|
||||
return inst;
|
||||
}
|
||||
|
||||
/** Parse and evaluate a hexpat source. Synchronous. */
|
||||
export function runHexpat(src: string, host: HexpatHost, opts: RunOptions = {}): PatternInstance {
|
||||
return evaluate(compile(src, host, opts.name, opts.defines), host, opts.hooks);
|
||||
|
||||
+60
-2
@@ -8,6 +8,11 @@ export interface JsonOptions {
|
||||
maxEntries?: number; // cap array entries (default: all)
|
||||
}
|
||||
|
||||
export interface TreeOptions extends JsonOptions {
|
||||
color?: boolean; // color rows with ANSI truecolor escapes (default: off)
|
||||
patterns?: Pattern[]; // selected roots (default: all placed patterns)
|
||||
}
|
||||
|
||||
export interface PatternJson {
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -15,6 +20,10 @@ export interface PatternJson {
|
||||
addr: number;
|
||||
offset: number;
|
||||
size: number;
|
||||
displayName?: string;
|
||||
color?: string;
|
||||
comment?: string;
|
||||
attributes?: { name: string; args: any[] }[];
|
||||
local?: boolean;
|
||||
section?: number;
|
||||
bits?: number;
|
||||
@@ -46,6 +55,10 @@ export function patternToJson(inst: PatternInstance, p: Pattern, opts: JsonOptio
|
||||
size: p.size,
|
||||
};
|
||||
if (p.local) j.local = true;
|
||||
if (p.displayName !== undefined) j.displayName = p.displayName;
|
||||
if (p.getColor() !== undefined) j.color = p.getColor();
|
||||
if (p.comment !== undefined) j.comment = p.comment;
|
||||
if (p.attrs && p.attrs.length) j.attributes = attributes(inst, p);
|
||||
if (p.section !== MAIN_SECTION && !p.local) j.section = p.section;
|
||||
if (p.kind === "bitfield_field") { j.bits = p.bits; j.bitOffset = p.bitPos % 8; }
|
||||
if (p.kind === "array") {
|
||||
@@ -74,12 +87,57 @@ export function toJson(inst: PatternInstance, opts: JsonOptions = {}): PatternJs
|
||||
return inst.patterns.filter((p) => !p.hidden).map((p) => patternToJson(inst, p, opts));
|
||||
}
|
||||
|
||||
/** Evaluated, JSON-safe attribute arguments, including pattern-valued arguments. */
|
||||
function attributes(inst: PatternInstance, p: Pattern): { name: string; args: any[] }[] {
|
||||
return (p.attrs || []).map((a) => ({ name: a.name, args: a.args.map((v) => {
|
||||
const value = inst.decay(v);
|
||||
return value instanceof Pattern ? inst.toStr(value) : jsonValue(value);
|
||||
}) }));
|
||||
}
|
||||
|
||||
/** Hexpat's RRGGBB colors in a form usable by ANSI and r2's rgb: syntax. */
|
||||
export function patternColor(p: Pattern): string | undefined {
|
||||
const m = (p.getColor() || "").match(/^(?:#|0x|rgb:)?([0-9a-f]{6})$/i);
|
||||
return m ? m[1].toLowerCase() : undefined;
|
||||
}
|
||||
|
||||
/** Draw a tree with values, addresses and evaluated presentation attributes. */
|
||||
export function dumpTree(inst: PatternInstance, print: (s: string) => void, opts: TreeOptions = {}): void {
|
||||
const walk = (p: Pattern, prefix: string, branch: string): void => {
|
||||
const attrs = attributes(inst, p).filter((a) => !["color", "comment", "name"].includes(a.name));
|
||||
const presentation: [string, string | undefined][] = [["color", p.getColor()], ["comment", p.comment], ["name", p.displayName]];
|
||||
for (const [name, value] of presentation) {
|
||||
if (value !== undefined) attrs.push({ name, args: [value] });
|
||||
}
|
||||
let line = inst.describePattern(p, opts.base || 0);
|
||||
if (attrs.length) line += " [[" + attrs.map((a) => a.name + (a.args.length ? "(" +
|
||||
a.args.map((v) => JSON.stringify(v) || "null").join(", ") + ")" : "")).join(", ") + "]]";
|
||||
const rgb = opts.color ? patternColor(p) : undefined;
|
||||
if (rgb) line = "\x1b[38;2;" + [0, 2, 4].map((i) => parseInt(rgb.substring(i, i + 2), 16)).join(";") + "m" + line + "\x1b[0m";
|
||||
print(prefix + branch + line);
|
||||
if (p.sealed) return;
|
||||
let children: Pattern[] = [];
|
||||
let omitted = 0;
|
||||
if (p.kind === "pointer") children = p.pointee ? [p.pointee] : [];
|
||||
else if (p.kind === "array") {
|
||||
const n = p.entryCount(), max = opts.maxEntries === undefined ? n : Math.min(n, opts.maxEntries);
|
||||
for (let i = 0; i < max; i++) children.push(p.entry(i)!);
|
||||
omitted = n - max;
|
||||
} else if (p.isComposite()) children = p.children || [];
|
||||
children = children.filter((c) => !c.hidden);
|
||||
const next = prefix + (branch ? branch === "`- " ? " " : "| " : "");
|
||||
children.forEach((c, i) => walk(c, next, i === children.length - 1 && !omitted ? "`- " : "|- "));
|
||||
if (omitted) print(next + "`- ... (" + omitted + " more entries)");
|
||||
};
|
||||
for (const p of opts.patterns || inst.patterns) if (!p.hidden) walk(p, "", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the placed patterns depth first. `path` holds the member names from
|
||||
* the top-level pattern down (array entries use their index).
|
||||
* Return false from `visit` to skip the children of a pattern.
|
||||
*/
|
||||
export function walkPatterns(inst: PatternInstance, visit: (p: Pattern, path: string[]) => boolean | void): void {
|
||||
export function walkPatterns(inst: PatternInstance, visit: (p: Pattern, path: string[]) => boolean | void, patterns = inst.patterns): void {
|
||||
const walk = (p: Pattern, path: string[]) => {
|
||||
if (p.hidden) return;
|
||||
if (visit(p, path) === false) return;
|
||||
@@ -92,5 +150,5 @@ export function walkPatterns(inst: PatternInstance, visit: (p: Pattern, path: st
|
||||
for (const c of p.children || []) walk(c, path.concat(c.name));
|
||||
}
|
||||
};
|
||||
for (const p of inst.patterns) walk(p, [p.name]);
|
||||
for (const p of patterns) walk(p, [p.name]);
|
||||
}
|
||||
@@ -67,6 +67,11 @@ export class Pattern {
|
||||
return this.kind === "struct" || this.kind === "union" || this.kind === "array" || this.kind === "bitfield";
|
||||
}
|
||||
|
||||
/** Explicit member colors override the enclosing aggregate's color. */
|
||||
getColor(): string | undefined {
|
||||
return this.color === undefined ? this.parent?.getColor() : this.color;
|
||||
}
|
||||
|
||||
attr(name: string): { name: string; args: any[] } | undefined {
|
||||
if (this.attrs) for (const a of this.attrs) if (a.name === name) return a;
|
||||
return undefined;
|
||||
|
||||
+7
-1
@@ -230,7 +230,13 @@ const BUILTINS: Record<string, Fn> = {
|
||||
"std::core::get_attribute_value": (ev, a) => BUILTINS["std::core::get_attribute_argument"](ev, a, ev.loc),
|
||||
"std::core::set_display_name": (ev, a) => { if (a[0] instanceof Pattern) a[0].name = ev.toStr(ev.decay(a[1])); },
|
||||
"std::core::set_pattern_comment": (ev, a) => { if (a[0] instanceof Pattern) a[0].comment = ev.toStr(ev.decay(a[1])); },
|
||||
"std::core::set_pattern_color": () => undefined,
|
||||
"std::core::set_pattern_color": (ev, a) => {
|
||||
if (a[0] instanceof Pattern) {
|
||||
// The runtime's integer colors store red in the least significant byte.
|
||||
const color = uintN(32, ev.toInt(a[1]));
|
||||
a[0].color = [0n, 8n, 16n].map((shift) => Number((color >> shift) & 0xffn).toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
},
|
||||
"std::core::set_endian": (ev, a) => { const e = ev.toInt(a[0]); ev.bigEndian = e === 1n; },
|
||||
"std::core::get_endian": (ev) => (ev.bigEndian ? 1n : 2n),
|
||||
"std::core::array_index": (ev) => ev.arrayIndex,
|
||||
|
||||
@@ -0,0 +1,861 @@
|
||||
// Game Boy / Game Boy Color cartridge and CPU memory layouts.
|
||||
// Reference: https://gbdev.io/pandocs/ (CC0 hardware documentation).
|
||||
// Header: https://gbdev.io/pandocs/The_Cartridge_Header.html
|
||||
// I/O: https://gbdev.io/pandocs/Hardware_Reg_List.html
|
||||
// Memory: https://gbdev.io/pandocs/Memory_Map.html
|
||||
// Mappers: https://gbdev.io/pandocs/MBCs.html
|
||||
// Plugin: https://github.com/radareorg/r2hexpat
|
||||
// ROM: r2 -q -c 'hexpat gameboy.hexpat' game.gb
|
||||
// Apply flags, data hints and comments: .hexpat* gameboy.hexpat
|
||||
// JSON: hexpatj gameboy.hexpat
|
||||
// Set GB_CPU_MEMORY to 1 ONLY for a flat 64 KiB CPU address-space dump.
|
||||
// A ROM file's offsets above 0x7FFF are more ROM banks, not CPU RAM or I/O.
|
||||
// CPU dumps expose only the currently selected ROM/VRAM/WRAM/cartridge bank.
|
||||
// This maps data; it does not emulate devices or reconstruct bank state.
|
||||
|
||||
#pragma endian little
|
||||
#pragma base_address 0
|
||||
|
||||
#define GB_CPU_MEMORY 0
|
||||
|
||||
// Bitfields below run from least significant bit to most significant bit.
|
||||
// CGB flag bit 7 enables color mode; 0xC0 conventionally means CGB-only.
|
||||
// 0x00 requests DMG compatibility. 0xCE and 0xC3 are NOT DMG/CGB markers.
|
||||
enum CgbFlag : u8 {
|
||||
DmgCompatible = 0x00 ... 0x7F,
|
||||
CgbEnhanced = 0x80,
|
||||
CgbOnly = 0xC0
|
||||
};
|
||||
|
||||
enum SgbFlag : u8 {
|
||||
NoSgb = 0x00,
|
||||
SgbSupported = 0x03
|
||||
};
|
||||
|
||||
enum CartridgeType : u8 {
|
||||
RomOnly = 0x00,
|
||||
Mbc1 = 0x01,
|
||||
Mbc1Ram = 0x02,
|
||||
Mbc1RamBattery = 0x03,
|
||||
Mbc2 = 0x05,
|
||||
Mbc2Battery = 0x06,
|
||||
RomRam = 0x08,
|
||||
RomRamBattery = 0x09,
|
||||
Mmm01 = 0x0B,
|
||||
Mmm01Ram = 0x0C,
|
||||
Mmm01RamBattery = 0x0D,
|
||||
Mbc3TimerBattery = 0x0F,
|
||||
Mbc3TimerRamBattery = 0x10,
|
||||
Mbc3 = 0x11,
|
||||
Mbc3Ram = 0x12,
|
||||
Mbc3RamBattery = 0x13,
|
||||
Mbc5 = 0x19,
|
||||
Mbc5Ram = 0x1A,
|
||||
Mbc5RamBattery = 0x1B,
|
||||
Mbc5Rumble = 0x1C,
|
||||
Mbc5RumbleRam = 0x1D,
|
||||
Mbc5RumbleRamBattery = 0x1E,
|
||||
Mbc6 = 0x20,
|
||||
Mbc7SensorRumbleRamBattery = 0x22,
|
||||
PocketCamera = 0xFC,
|
||||
BandaiTama5 = 0xFD,
|
||||
HuC3 = 0xFE,
|
||||
HuC1RamBattery = 0xFF
|
||||
};
|
||||
|
||||
enum RomSize : u8 {
|
||||
KiB32_Banks2 = 0x00,
|
||||
KiB64_Banks4 = 0x01,
|
||||
KiB128_Banks8 = 0x02,
|
||||
KiB256_Banks16 = 0x03,
|
||||
KiB512_Banks32 = 0x04,
|
||||
MiB1_Banks64 = 0x05,
|
||||
MiB2_Banks128 = 0x06,
|
||||
MiB4_Banks256 = 0x07,
|
||||
MiB8_Banks512 = 0x08,
|
||||
Unofficial_Banks72 = 0x52,
|
||||
Unofficial_Banks80 = 0x53,
|
||||
Unofficial_Banks96 = 0x54
|
||||
};
|
||||
|
||||
// 0x01 is a legacy unofficial 2 KiB code, unused by licensed cartridges.
|
||||
// MBC2 has 512 four-bit RAM cells despite its header RAM size being zero.
|
||||
enum RamSize : u8 {
|
||||
None = 0x00,
|
||||
Unused_LegacyKiB2 = 0x01,
|
||||
KiB8_Banks1 = 0x02,
|
||||
KiB32_Banks4 = 0x03,
|
||||
KiB128_Banks16 = 0x04,
|
||||
KiB64_Banks8 = 0x05
|
||||
};
|
||||
|
||||
enum Destination : u8 {
|
||||
Japan = 0x00,
|
||||
Overseas = 0x01
|
||||
};
|
||||
|
||||
// New codes are two ASCII bytes; read this enum big-endian.
|
||||
enum NewLicenseeCode : u16 {
|
||||
None_00 = 0x3030,
|
||||
Nintendo_Research_Development_1_01 = 0x3031,
|
||||
Capcom_08 = 0x3038,
|
||||
EA_Electronic_Arts_13 = 0x3133,
|
||||
Hudson_Soft_18 = 0x3138,
|
||||
B_AI_19 = 0x3139,
|
||||
KSS_20 = 0x3230,
|
||||
Planning_Office_WADA_22 = 0x3232,
|
||||
PCM_Complete_24 = 0x3234,
|
||||
San_X_25 = 0x3235,
|
||||
Kemco_28 = 0x3238,
|
||||
SETA_Corporation_29 = 0x3239,
|
||||
Viacom_30 = 0x3330,
|
||||
Nintendo_31 = 0x3331,
|
||||
Bandai_32 = 0x3332,
|
||||
Ocean_Software_Acclaim_Entertainment_33 = 0x3333,
|
||||
Konami_34 = 0x3334,
|
||||
HectorSoft_35 = 0x3335,
|
||||
Taito_37 = 0x3337,
|
||||
Hudson_Soft_38 = 0x3338,
|
||||
Banpresto_39 = 0x3339,
|
||||
Ubi_Soft_41 = 0x3431,
|
||||
Atlus_42 = 0x3432,
|
||||
Malibu_Interactive_44 = 0x3434,
|
||||
Angel_46 = 0x3436,
|
||||
Bullet_Proof_Software_47 = 0x3437,
|
||||
Irem_49 = 0x3439,
|
||||
Absolute_50 = 0x3530,
|
||||
Acclaim_Entertainment_51 = 0x3531,
|
||||
Activision_52 = 0x3532,
|
||||
Sammy_USA_Corporation_53 = 0x3533,
|
||||
Konami_54 = 0x3534,
|
||||
Hi_Tech_Expressions_55 = 0x3535,
|
||||
LJN_56 = 0x3536,
|
||||
Matchbox_57 = 0x3537,
|
||||
Mattel_58 = 0x3538,
|
||||
Milton_Bradley_Company_59 = 0x3539,
|
||||
Titus_Interactive_60 = 0x3630,
|
||||
Virgin_Games_Ltd_61 = 0x3631,
|
||||
Lucasfilm_Games_64 = 0x3634,
|
||||
Ocean_Software_67 = 0x3637,
|
||||
EA_Electronic_Arts_69 = 0x3639,
|
||||
Infogrames_70 = 0x3730,
|
||||
Interplay_Entertainment_71 = 0x3731,
|
||||
Broderbund_72 = 0x3732,
|
||||
Sculptured_Software_73 = 0x3733,
|
||||
The_Sales_Curve_Limited_75 = 0x3735,
|
||||
THQ_78 = 0x3738,
|
||||
Accolade_79 = 0x3739,
|
||||
Misawa_Entertainment_80 = 0x3830,
|
||||
LOZC_G_83 = 0x3833,
|
||||
Tokuma_Shoten_86 = 0x3836,
|
||||
Tsukuda_Original_87 = 0x3837,
|
||||
Chunsoft_Co_91 = 0x3931,
|
||||
Video_System_92 = 0x3932,
|
||||
Ocean_Software_Acclaim_Entertainment_93 = 0x3933,
|
||||
Varie_95 = 0x3935,
|
||||
Yonezawa_S_Pal_96 = 0x3936,
|
||||
Kaneko_97 = 0x3937,
|
||||
Pack_In_Video_99 = 0x3939,
|
||||
Bottom_Up_9H = 0x3948,
|
||||
Konami_Yu_Gi_Oh_A4 = 0x4134,
|
||||
MTO_BL = 0x424C,
|
||||
Kodansha_DK = 0x444B
|
||||
};
|
||||
|
||||
enum OldLicenseeCode : u8 {
|
||||
None_00 = 0x00,
|
||||
Nintendo_01 = 0x01,
|
||||
Capcom_08 = 0x08,
|
||||
HOT_B_09 = 0x09,
|
||||
Jaleco_0A = 0x0A,
|
||||
Coconuts_Japan_0B = 0x0B,
|
||||
Elite_Systems_0C = 0x0C,
|
||||
EA_Electronic_Arts_13 = 0x13,
|
||||
Hudson_Soft_18 = 0x18,
|
||||
ITC_Entertainment_19 = 0x19,
|
||||
Yanoman_1A = 0x1A,
|
||||
Japan_Clary_1D = 0x1D,
|
||||
Virgin_Games_Ltd_1F = 0x1F,
|
||||
PCM_Complete_24 = 0x24,
|
||||
San_X_25 = 0x25,
|
||||
Kemco_28 = 0x28,
|
||||
SETA_Corporation_29 = 0x29,
|
||||
Infogrames_30 = 0x30,
|
||||
Nintendo_31 = 0x31,
|
||||
Bandai_32 = 0x32,
|
||||
UseNewLicensee_33 = 0x33,
|
||||
Konami_34 = 0x34,
|
||||
HectorSoft_35 = 0x35,
|
||||
Capcom_38 = 0x38,
|
||||
Banpresto_39 = 0x39,
|
||||
Entertainment_Interactive_3C = 0x3C,
|
||||
Gremlin_3E = 0x3E,
|
||||
Ubi_Soft_41 = 0x41,
|
||||
Atlus_42 = 0x42,
|
||||
Malibu_Interactive_44 = 0x44,
|
||||
Angel_46 = 0x46,
|
||||
Spectrum_HoloByte_47 = 0x47,
|
||||
Irem_49 = 0x49,
|
||||
Virgin_Games_Ltd_4A = 0x4A,
|
||||
Malibu_Interactive_4D = 0x4D,
|
||||
U_S_Gold_4F = 0x4F,
|
||||
Absolute_50 = 0x50,
|
||||
Acclaim_Entertainment_51 = 0x51,
|
||||
Activision_52 = 0x52,
|
||||
Sammy_USA_Corporation_53 = 0x53,
|
||||
GameTek_54 = 0x54,
|
||||
Park_Place_55 = 0x55,
|
||||
LJN_56 = 0x56,
|
||||
Matchbox_57 = 0x57,
|
||||
Milton_Bradley_Company_59 = 0x59,
|
||||
Mindscape_5A = 0x5A,
|
||||
Romstar_5B = 0x5B,
|
||||
Naxat_Soft_5C = 0x5C,
|
||||
Tradewest_5D = 0x5D,
|
||||
Titus_Interactive_60 = 0x60,
|
||||
Virgin_Games_Ltd_61 = 0x61,
|
||||
Ocean_Software_67 = 0x67,
|
||||
EA_Electronic_Arts_69 = 0x69,
|
||||
Elite_Systems_6E = 0x6E,
|
||||
Electro_Brain_6F = 0x6F,
|
||||
Infogrames_70 = 0x70,
|
||||
Interplay_Entertainment_71 = 0x71,
|
||||
Broderbund_72 = 0x72,
|
||||
Sculptured_Software_73 = 0x73,
|
||||
The_Sales_Curve_Limited_75 = 0x75,
|
||||
THQ_78 = 0x78,
|
||||
Accolade_79 = 0x79,
|
||||
Triffix_Entertainment_7A = 0x7A,
|
||||
MicroProse_7C = 0x7C,
|
||||
Kemco_7F = 0x7F,
|
||||
Misawa_Entertainment_80 = 0x80,
|
||||
LOZC_G_83 = 0x83,
|
||||
Tokuma_Shoten_86 = 0x86,
|
||||
Bullet_Proof_Software_8B = 0x8B,
|
||||
Vic_Tokai_Corp_8C = 0x8C,
|
||||
Ape_Inc_8E = 0x8E,
|
||||
I_Max_8F = 0x8F,
|
||||
Chunsoft_Co_91 = 0x91,
|
||||
Video_System_92 = 0x92,
|
||||
Tsubaraya_Productions_93 = 0x93,
|
||||
Varie_95 = 0x95,
|
||||
Yonezawa_S_Pal_96 = 0x96,
|
||||
Kemco_97 = 0x97,
|
||||
Arc_99 = 0x99,
|
||||
Nihon_Bussan_9A = 0x9A,
|
||||
Tecmo_9B = 0x9B,
|
||||
Imagineer_9C = 0x9C,
|
||||
Banpresto_9D = 0x9D,
|
||||
Nova_9F = 0x9F,
|
||||
Hori_Electric_A1 = 0xA1,
|
||||
Bandai_A2 = 0xA2,
|
||||
Konami_A4 = 0xA4,
|
||||
Kawada_A6 = 0xA6,
|
||||
Takara_A7 = 0xA7,
|
||||
Technos_Japan_A9 = 0xA9,
|
||||
Broderbund_AA = 0xAA,
|
||||
Toei_Animation_AC = 0xAC,
|
||||
Toho_AD = 0xAD,
|
||||
Namco_AF = 0xAF,
|
||||
Acclaim_Entertainment_B0 = 0xB0,
|
||||
ASCII_Corporation_or_Nexsoft_B1 = 0xB1,
|
||||
Bandai_B2 = 0xB2,
|
||||
Square_Enix_B4 = 0xB4,
|
||||
HAL_Laboratory_B6 = 0xB6,
|
||||
SNK_B7 = 0xB7,
|
||||
Pony_Canyon_B9 = 0xB9,
|
||||
Culture_Brain_BA = 0xBA,
|
||||
Sunsoft_BB = 0xBB,
|
||||
Sony_Imagesoft_BD = 0xBD,
|
||||
Sammy_Corporation_BF = 0xBF,
|
||||
Taito_C0 = 0xC0,
|
||||
Kemco_C2 = 0xC2,
|
||||
Square_C3 = 0xC3,
|
||||
Tokuma_Shoten_C4 = 0xC4,
|
||||
Data_East_C5 = 0xC5,
|
||||
Tonkin_House_C6 = 0xC6,
|
||||
Koei_C8 = 0xC8,
|
||||
UFL_C9 = 0xC9,
|
||||
Ultra_Games_CA = 0xCA,
|
||||
VAP_Inc_CB = 0xCB,
|
||||
Use_Corporation_CC = 0xCC,
|
||||
Meldac_CD = 0xCD,
|
||||
Pony_Canyon_CE = 0xCE,
|
||||
Angel_CF = 0xCF,
|
||||
Taito_D0 = 0xD0,
|
||||
SOFEL_Software_Engineering_Lab_D1 = 0xD1,
|
||||
Quest_D2 = 0xD2,
|
||||
Sigma_Enterprises_D3 = 0xD3,
|
||||
ASK_Kodansha_Co_D4 = 0xD4,
|
||||
Naxat_Soft_D6 = 0xD6,
|
||||
Copya_System_D7 = 0xD7,
|
||||
Banpresto_D9 = 0xD9,
|
||||
Tomy_DA = 0xDA,
|
||||
LJN_DB = 0xDB,
|
||||
Nippon_Computer_Systems_DD = 0xDD,
|
||||
Human_Ent_DE = 0xDE,
|
||||
Altron_DF = 0xDF,
|
||||
Jaleco_E0 = 0xE0,
|
||||
Towa_Chiki_E1 = 0xE1,
|
||||
Yutaka_E2 = 0xE2,
|
||||
Varie_E3 = 0xE3,
|
||||
Epoch_E5 = 0xE5,
|
||||
Athena_E7 = 0xE7,
|
||||
Asmik_Ace_Entertainment_E8 = 0xE8,
|
||||
Natsume_E9 = 0xE9,
|
||||
King_Records_EA = 0xEA,
|
||||
Atlus_EB = 0xEB,
|
||||
Epic_Sony_Records_EC = 0xEC,
|
||||
IGS_EE = 0xEE,
|
||||
A_Wave_F0 = 0xF0,
|
||||
Extreme_Entertainment_F3 = 0xF3
|
||||
};
|
||||
|
||||
// All three title layouts occupy the same 16 bytes; no reliable auto-detection.
|
||||
struct CgbTitle {
|
||||
char text[15];
|
||||
CgbFlag cgb_flag;
|
||||
};
|
||||
|
||||
struct ManufacturerTitle {
|
||||
char text[11];
|
||||
char manufacturer_code[4];
|
||||
CgbFlag cgb_flag;
|
||||
};
|
||||
|
||||
union CartridgeTitle {
|
||||
char dmg_text[16];
|
||||
CgbTitle cgb;
|
||||
ManufacturerTitle manufacturer;
|
||||
};
|
||||
|
||||
union NewLicensee {
|
||||
char ascii[2];
|
||||
be NewLicenseeCode publisher;
|
||||
};
|
||||
|
||||
struct GameBoyHeader {
|
||||
u8 entry_point_code[4]; // 0100: instructions, not an integer pointer.
|
||||
u8 nintendo_logo[48]; // 0104: boot logo, ends at 0133.
|
||||
CartridgeTitle title; // 0134: overlapping DMG/CGB title layouts.
|
||||
NewLicensee new_licensee; // 0144: used only when old_licensee == 0x33.
|
||||
SgbFlag sgb_flag; // 0146: SGB also requires old_licensee 0x33.
|
||||
CartridgeType cartridge_type; // 0147.
|
||||
RomSize rom_size; // 0148.
|
||||
RamSize ram_size; // 0149.
|
||||
Destination destination; // 014A.
|
||||
OldLicenseeCode old_licensee; // 014B.
|
||||
u8 rom_version; // 014C.
|
||||
u8 header_checksum; // 014D: subtract each byte 0134..014C plus 1.
|
||||
be u16 global_checksum; // 014E: sum ROM bytes excluding 014E..014F.
|
||||
};
|
||||
|
||||
// These slots contain code, not address pointers; unused vectors may hold data.
|
||||
struct GameBoyVectors {
|
||||
u8 rst_00[8];
|
||||
u8 rst_08[8];
|
||||
u8 rst_10[8];
|
||||
u8 rst_18[8];
|
||||
u8 rst_20[8];
|
||||
u8 rst_28[8];
|
||||
u8 rst_30[8];
|
||||
u8 rst_38[8];
|
||||
u8 interrupt_vblank_40[8];
|
||||
u8 interrupt_lcd_stat_48[8];
|
||||
u8 interrupt_timer_50[8];
|
||||
u8 interrupt_serial_58[8];
|
||||
u8 interrupt_joypad_60[8];
|
||||
};
|
||||
|
||||
// Register layouts describe bit positions, not read/write side effects.
|
||||
// Unused bits often read as 1; write-only bits cannot be recovered from a dump.
|
||||
enum TimerClock : u8 {
|
||||
Hz4096 = 0,
|
||||
Hz262144 = 1,
|
||||
Hz65536 = 2,
|
||||
Hz16384 = 3
|
||||
};
|
||||
|
||||
enum PpuMode : u8 {
|
||||
HBlank = 0,
|
||||
VBlank = 1,
|
||||
OamScan = 2,
|
||||
Drawing = 3
|
||||
};
|
||||
|
||||
enum DmgShade : u8 {
|
||||
White = 0,
|
||||
LightGray = 1,
|
||||
DarkGray = 2,
|
||||
Black = 3
|
||||
};
|
||||
|
||||
enum DutyCycle : u8 {
|
||||
Percent12_5 = 0,
|
||||
Percent25 = 1,
|
||||
Percent50 = 2,
|
||||
Percent75 = 3
|
||||
};
|
||||
|
||||
enum WaveLevel : u8 {
|
||||
Mute = 0,
|
||||
Full = 1,
|
||||
Half = 2,
|
||||
Quarter = 3
|
||||
};
|
||||
|
||||
bitfield Joypad {
|
||||
a_or_right_released : 1;
|
||||
b_or_left_released : 1;
|
||||
select_or_up_released : 1;
|
||||
start_or_down_released : 1;
|
||||
dpad_deselected : 1; // 0 selects directions.
|
||||
buttons_deselected : 1; // 0 selects buttons; all inputs active-low.
|
||||
padding : 2;
|
||||
};
|
||||
|
||||
bitfield SerialControl {
|
||||
internal_clock : 1;
|
||||
cgb_fast_clock : 1;
|
||||
padding : 5;
|
||||
transfer_requested : 1;
|
||||
};
|
||||
|
||||
bitfield TimerControl {
|
||||
TimerClock clock : 2; // Normal-speed rates; double on CGB double speed.
|
||||
enabled : 1;
|
||||
padding : 5;
|
||||
};
|
||||
|
||||
bitfield Interrupts {
|
||||
vblank : 1;
|
||||
lcd_stat : 1;
|
||||
timer : 1;
|
||||
serial : 1;
|
||||
joypad : 1;
|
||||
padding : 3;
|
||||
};
|
||||
|
||||
bitfield AudioSweep {
|
||||
shift : 3;
|
||||
subtract : 1;
|
||||
pace : 3;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield AudioDutyLength {
|
||||
initial_length_timer : 6; // Length = 64 - value; write-only portion.
|
||||
DutyCycle duty : 2;
|
||||
};
|
||||
|
||||
bitfield AudioEnvelope {
|
||||
pace : 3;
|
||||
increase : 1;
|
||||
initial_volume : 4;
|
||||
};
|
||||
|
||||
bitfield AudioPeriodHigh {
|
||||
period_high : 3;
|
||||
padding : 3;
|
||||
length_enabled : 1;
|
||||
trigger : 1; // Write 1 to trigger; not a readable state bit.
|
||||
};
|
||||
|
||||
bitfield WaveDac {
|
||||
padding : 7;
|
||||
enabled : 1;
|
||||
};
|
||||
|
||||
bitfield WaveOutputLevel {
|
||||
padding : 5;
|
||||
WaveLevel level : 2;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield NoiseLength {
|
||||
initial_length_timer : 6;
|
||||
padding : 2;
|
||||
};
|
||||
|
||||
bitfield NoisePolynomial {
|
||||
clock_divider : 3; // Divisors: 8,16,32,48,64,80,96,112.
|
||||
short_lfsr : 1; // 1: 7-bit; 0: 15-bit.
|
||||
clock_shift : 4;
|
||||
};
|
||||
|
||||
bitfield NoiseControl {
|
||||
padding : 6;
|
||||
length_enabled : 1;
|
||||
trigger : 1;
|
||||
};
|
||||
|
||||
bitfield AudioVolume {
|
||||
right_volume : 3;
|
||||
vin_right : 1;
|
||||
left_volume : 3;
|
||||
vin_left : 1;
|
||||
};
|
||||
|
||||
bitfield AudioPanning {
|
||||
channel1_right : 1;
|
||||
channel2_right : 1;
|
||||
channel3_right : 1;
|
||||
channel4_right : 1;
|
||||
channel1_left : 1;
|
||||
channel2_left : 1;
|
||||
channel3_left : 1;
|
||||
channel4_left : 1;
|
||||
};
|
||||
|
||||
bitfield AudioMaster {
|
||||
channel1_active : 1; // Bits 0..3 read-only.
|
||||
channel2_active : 1;
|
||||
channel3_active : 1;
|
||||
channel4_active : 1;
|
||||
padding : 3;
|
||||
audio_enabled : 1;
|
||||
};
|
||||
|
||||
bitfield WaveSamples {
|
||||
second_sample : 4;
|
||||
first_sample : 4;
|
||||
};
|
||||
|
||||
bitfield LcdControl {
|
||||
bg_window_enable_or_cgb_priority : 1;
|
||||
objects_enabled : 1;
|
||||
objects_8x16 : 1;
|
||||
bg_map_9c00 : 1;
|
||||
tile_data_8000_unsigned : 1; // 0: signed IDs relative to 9000.
|
||||
window_enabled : 1;
|
||||
window_map_9c00 : 1;
|
||||
lcd_enabled : 1;
|
||||
};
|
||||
|
||||
bitfield LcdStatus {
|
||||
PpuMode mode : 2; // Read-only.
|
||||
ly_equals_lyc : 1; // Read-only.
|
||||
hblank_interrupt : 1;
|
||||
vblank_interrupt : 1;
|
||||
oam_interrupt : 1;
|
||||
lyc_interrupt : 1;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield DmgPalette {
|
||||
DmgShade color0 : 2; // Ignored for OBP0/OBP1: OBJ index 0 is transparent.
|
||||
DmgShade color1 : 2;
|
||||
DmgShade color2 : 2;
|
||||
DmgShade color3 : 2;
|
||||
};
|
||||
|
||||
bitfield CpuMode {
|
||||
padding : 2;
|
||||
dmg_compatibility : 1; // Boot-ROM-only; locked when boot ROM is unmapped.
|
||||
unknown : 5;
|
||||
};
|
||||
|
||||
bitfield SpeedSwitch {
|
||||
switch_armed : 1; // Arm, then execute STOP.
|
||||
padding : 6;
|
||||
double_speed : 1; // Read-only.
|
||||
};
|
||||
|
||||
bitfield VramBank {
|
||||
bank : 1;
|
||||
padding : 7;
|
||||
};
|
||||
|
||||
bitfield VramDmaControl {
|
||||
blocks_minus_one : 7; // Transfer length = (value + 1) * 16 bytes.
|
||||
hblank_on_write_inactive_on_read : 1;
|
||||
};
|
||||
|
||||
bitfield Infrared {
|
||||
led_on : 1;
|
||||
no_signal : 1; // Read-only, 0 means receiving.
|
||||
padding : 4;
|
||||
read_enable : 2; // 3 enables receiver; 0 disables it.
|
||||
};
|
||||
|
||||
bitfield PaletteIndex {
|
||||
byte_index : 6;
|
||||
padding : 1;
|
||||
auto_increment_on_write : 1;
|
||||
};
|
||||
|
||||
bitfield ObjectPriority {
|
||||
dmg_x_priority : 1; // 0: CGB OAM order; 1: DMG x-coordinate order.
|
||||
padding : 7;
|
||||
};
|
||||
|
||||
bitfield WramBank {
|
||||
bank : 3; // 0 aliases bank 1.
|
||||
padding : 5;
|
||||
};
|
||||
|
||||
bitfield Undocumented75 {
|
||||
padding : 4;
|
||||
storage : 3;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield Pcm12 {
|
||||
channel1 : 4;
|
||||
channel2 : 4;
|
||||
};
|
||||
|
||||
bitfield Pcm34 {
|
||||
channel3 : 4;
|
||||
channel4 : 4;
|
||||
};
|
||||
|
||||
// Offsets below are relative to FF00; gaps preserve exact register addresses.
|
||||
// CGB-only fields are retained in DMG dumps for address consistency.
|
||||
struct GameBoyIo {
|
||||
Joypad JOYP; // FF00: joypad / SGB packet transport.
|
||||
u8 SB; // FF01: serial data.
|
||||
SerialControl SC; // FF02.
|
||||
u8 unused_ff03;
|
||||
u8 DIV; // FF04: any write resets divider.
|
||||
u8 TIMA; // FF05: timer counter.
|
||||
u8 TMA; // FF06: timer reload value.
|
||||
TimerControl TAC; // FF07.
|
||||
u8 unused_ff08_ff0e[7];
|
||||
Interrupts IF; // FF0F: requested interrupts.
|
||||
AudioSweep NR10; // FF10: channel 1.
|
||||
AudioDutyLength NR11; // FF11.
|
||||
AudioEnvelope NR12; // FF12.
|
||||
u8 NR13; // FF13: period low, write-only.
|
||||
AudioPeriodHigh NR14; // FF14.
|
||||
u8 unused_ff15;
|
||||
AudioDutyLength NR21; // FF16: channel 2.
|
||||
AudioEnvelope NR22; // FF17.
|
||||
u8 NR23; // FF18: period low, write-only.
|
||||
AudioPeriodHigh NR24; // FF19.
|
||||
WaveDac NR30; // FF1A: channel 3.
|
||||
u8 NR31; // FF1B: length = 256 - value, write-only.
|
||||
WaveOutputLevel NR32; // FF1C.
|
||||
u8 NR33; // FF1D: period low, write-only.
|
||||
AudioPeriodHigh NR34; // FF1E.
|
||||
u8 unused_ff1f;
|
||||
NoiseLength NR41; // FF20: channel 4.
|
||||
AudioEnvelope NR42; // FF21.
|
||||
NoisePolynomial NR43; // FF22.
|
||||
NoiseControl NR44; // FF23.
|
||||
AudioVolume NR50; // FF24.
|
||||
AudioPanning NR51; // FF25.
|
||||
AudioMaster NR52; // FF26.
|
||||
u8 unused_ff27_ff2f[9];
|
||||
WaveSamples wave_ram[16]; // FF30..FF3F: 32 four-bit samples.
|
||||
LcdControl LCDC; // FF40.
|
||||
LcdStatus STAT; // FF41.
|
||||
u8 SCY; // FF42: scroll y.
|
||||
u8 SCX; // FF43: scroll x.
|
||||
u8 LY; // FF44: scanline, read-only.
|
||||
u8 LYC; // FF45: scanline compare.
|
||||
u8 DMA; // FF46: write source high byte; copies 160 bytes to OAM.
|
||||
DmgPalette BGP; // FF47: non-CGB background palette.
|
||||
DmgPalette OBP0; // FF48: non-CGB object palette 0.
|
||||
DmgPalette OBP1; // FF49: non-CGB object palette 1.
|
||||
u8 WY; // FF4A: window y.
|
||||
u8 WX; // FF4B: window x + 7.
|
||||
CpuMode KEY0; // FF4C: CGB.
|
||||
SpeedSwitch KEY1; // FF4D: CGB.
|
||||
u8 unused_ff4e;
|
||||
VramBank VBK; // FF4F: CGB.
|
||||
u8 BOOT; // FF50: boot ROM unmap, write-only.
|
||||
u8 HDMA1; // FF51: CGB DMA source high, write-only.
|
||||
u8 HDMA2; // FF52: source low, low nibble ignored, write-only.
|
||||
u8 HDMA3; // FF53: destination bits 12..8, write-only.
|
||||
u8 HDMA4; // FF54: destination bits 7..4, write-only.
|
||||
VramDmaControl HDMA5; // FF55: CGB.
|
||||
Infrared RP; // FF56: CGB infrared.
|
||||
u8 unused_ff57_ff67[17];
|
||||
PaletteIndex BCPS; // FF68: CGB BG palette index (BGPI).
|
||||
u8 BCPD; // FF69: CGB BG palette byte (BGPD).
|
||||
PaletteIndex OCPS; // FF6A: CGB OBJ palette index (OBPI).
|
||||
u8 OCPD; // FF6B: CGB OBJ palette byte (OBPD).
|
||||
ObjectPriority OPRI; // FF6C: CGB.
|
||||
u8 unused_ff6d_ff6f[3];
|
||||
WramBank SVBK; // FF70: CGB.
|
||||
u8 unused_ff71;
|
||||
u8 undocumented_ff72; // CGB storage, R/W.
|
||||
u8 undocumented_ff73; // CGB storage, R/W.
|
||||
u8 undocumented_ff74; // CGB-mode storage, R/W.
|
||||
Undocumented75 undocumented_ff75;
|
||||
Pcm12 PCM12; // FF76: CGB channel 1/2 output, read-only.
|
||||
Pcm34 PCM34; // FF77: CGB channel 3/4 output, read-only.
|
||||
u8 unused_ff78_ff7f[8];
|
||||
};
|
||||
|
||||
// Graphics structures can also be placed manually over known ROM asset offsets.
|
||||
struct TileRow2bpp {
|
||||
u8 low_plane; // Bit 7 is the leftmost pixel.
|
||||
u8 high_plane; // Pixel index = low bit | (high bit << 1).
|
||||
};
|
||||
|
||||
struct Tile2bpp {
|
||||
TileRow2bpp rows[8];
|
||||
};
|
||||
|
||||
bitfield BgMapAttributes {
|
||||
cgb_palette : 3;
|
||||
tile_vram_bank : 1;
|
||||
padding : 1;
|
||||
x_flip : 1;
|
||||
y_flip : 1;
|
||||
bg_priority : 1;
|
||||
};
|
||||
|
||||
union TileMap {
|
||||
u8 tile_ids[1024]; // VRAM bank 0: 32 rows of 32 tile IDs.
|
||||
BgMapAttributes attributes[1024]; // VRAM bank 1: corresponding CGB attributes.
|
||||
};
|
||||
|
||||
struct GameBoyVram {
|
||||
Tile2bpp tiles[384]; // 8000..97FF.
|
||||
TileMap map_9800;
|
||||
TileMap map_9c00;
|
||||
};
|
||||
|
||||
bitfield ObjectAttributes {
|
||||
cgb_palette : 3;
|
||||
cgb_tile_bank : 1;
|
||||
dmg_palette_1 : 1;
|
||||
x_flip : 1;
|
||||
y_flip : 1;
|
||||
behind_bg_colors_1_to_3 : 1;
|
||||
};
|
||||
|
||||
struct OamObject {
|
||||
u8 y_plus_16;
|
||||
u8 x_plus_8;
|
||||
u8 tile_index; // In 8x16 mode, bit 0 is ignored.
|
||||
ObjectAttributes attributes;
|
||||
};
|
||||
|
||||
bitfield Rgb555 {
|
||||
red : 5;
|
||||
green : 5;
|
||||
blue : 5;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
struct CgbPalette {
|
||||
Rgb555 colors[4];
|
||||
};
|
||||
|
||||
// Not directly CPU-mapped: dump separately through BGPI/BGPD or OBPI/OBPD.
|
||||
// Each palette RAM bank contains eight palettes (64 bytes).
|
||||
struct CgbPaletteRam {
|
||||
CgbPalette palettes[8];
|
||||
};
|
||||
|
||||
// MBC control registers are WRITE destinations, not readable ROM structures.
|
||||
// These enums are reference constants; no control state is overlaid onto ROM.
|
||||
// Other cartridge types above need their own mapper-specific device protocols.
|
||||
enum Mbc1WriteAddress : u16 {
|
||||
RamEnable_0000_1fff = 0x0000, // Low nibble 0xA enables; anything else disables.
|
||||
RomBankLow5_2000_3fff = 0x2000, // 0 aliases 1 before masking to ROM size.
|
||||
BankHigh2_4000_5fff = 0x4000, // Upper ROM bits or RAM bank, depends on wiring.
|
||||
BankingMode_6000_7fff = 0x6000 // 0: simple; 1: advanced RAM/ROM banking.
|
||||
};
|
||||
|
||||
enum Mbc1Mode : u8 {
|
||||
Simple = 0,
|
||||
Advanced = 1
|
||||
};
|
||||
|
||||
enum RamEnableValue : u8 {
|
||||
Disable = 0x00,
|
||||
Enable = 0x0A
|
||||
};
|
||||
|
||||
// MBC2 decodes address bit 8 throughout 0000..3FFF, not two contiguous ranges.
|
||||
enum Mbc2WriteAddress : u16 {
|
||||
RamEnable_AddressBit8Clear = 0x0000,
|
||||
RomBankLow4_AddressBit8Set = 0x2100 // 0 aliases 1; RAM cells at A000..A1FF, mirrored.
|
||||
};
|
||||
|
||||
enum Mbc3WriteAddress : u16 {
|
||||
RamRtcEnable_0000_1fff = 0x0000,
|
||||
RomBank7_2000_3fff = 0x2000, // 0 aliases 1 (MBC30 uses 8 bits).
|
||||
RamRtcSelect_4000_5fff = 0x4000,
|
||||
LatchClock_6000_7fff = 0x6000 // Write 0, then 1, to latch RTC.
|
||||
};
|
||||
|
||||
enum Mbc3RamRtcSelect : u8 {
|
||||
RamBank0 = 0x00,
|
||||
RamBank1 = 0x01,
|
||||
RamBank2 = 0x02,
|
||||
RamBank3 = 0x03,
|
||||
Mbc30RamBank4 = 0x04,
|
||||
Mbc30RamBank5 = 0x05,
|
||||
Mbc30RamBank6 = 0x06,
|
||||
Mbc30RamBank7 = 0x07,
|
||||
RtcSeconds = 0x08,
|
||||
RtcMinutes = 0x09,
|
||||
RtcHours = 0x0A,
|
||||
RtcDayLow = 0x0B,
|
||||
RtcDayHigh = 0x0C
|
||||
};
|
||||
|
||||
bitfield RtcDayHigh {
|
||||
day_bit8 : 1;
|
||||
padding : 5;
|
||||
halted : 1;
|
||||
day_overflow : 1;
|
||||
};
|
||||
|
||||
// A convenient five-byte export layout, NOT five adjacent CPU registers.
|
||||
// Only the selected RTC register appears at A000..BFFF, repeated at every byte.
|
||||
struct Mbc3RtcSnapshot {
|
||||
u8 seconds;
|
||||
u8 minutes;
|
||||
u8 hours;
|
||||
u8 day_low;
|
||||
RtcDayHigh day_high;
|
||||
};
|
||||
|
||||
enum Mbc5WriteAddress : u16 {
|
||||
RamEnable_0000_1fff = 0x0000,
|
||||
RomBankLow8_2000_2fff = 0x2000, // Bank 0 is allowed.
|
||||
RomBankBit8_3000_3fff = 0x3000,
|
||||
RamBank4_4000_5fff = 0x4000 // Rumble carts: bit 3 motor, bits 0..2 RAM bank.
|
||||
};
|
||||
|
||||
bitfield Mbc5RumbleRamBank {
|
||||
ram_bank : 3;
|
||||
rumble_on : 1;
|
||||
padding : 4;
|
||||
};
|
||||
|
||||
// A raw ROM bank N starts at file offset N * 0x4000.
|
||||
// Its usual CPU address is 0x4000 + (file_offset % 0x4000) for N > 0.
|
||||
struct RomBank {
|
||||
u8 bytes[0x4000];
|
||||
};
|
||||
|
||||
if (GB_CPU_MEMORY) {
|
||||
std::assert (std::mem::size () >= 0x10000, "CPU memory mode requires a 64 KiB dump");
|
||||
GameBoyVectors vectors @ 0x0000;
|
||||
GameBoyHeader header @ 0x0100;
|
||||
u8 rom0_before_header[0x98] @ 0x0068;
|
||||
u8 rom0_after_header[0x3EB0] @ 0x0150;
|
||||
RomBank selected_rom_bank @ 0x4000;
|
||||
GameBoyVram vram @ 0x8000;
|
||||
u8 cartridge_ram_or_device[0x2000] @ 0xA000;
|
||||
u8 wram_bank0[0x1000] @ 0xC000;
|
||||
u8 selected_wram_bank[0x1000] @ 0xD000;
|
||||
u8 echo_c000_ddff[0x1E00] @ 0xE000;
|
||||
OamObject oam[40] @ 0xFE00;
|
||||
u8 unusable_fea0_feff[0x60] @ 0xFEA0;
|
||||
GameBoyIo io @ 0xFF00;
|
||||
u8 hram[0x7F] @ 0xFF80;
|
||||
Interrupts IE @ 0xFFFF;
|
||||
} else {
|
||||
std::assert (std::mem::size () >= 0x150, "ROM is too short for a Game Boy header");
|
||||
GameBoyVectors vectors @ 0x0000;
|
||||
GameBoyHeader header @ 0x0100;
|
||||
}
|
||||
+92
-11
@@ -3,7 +3,8 @@
|
||||
* plugin.ts - radare2 core plugin (r2js) running the hexpat library.
|
||||
* R2Host implements the library's HexpatHost interface on top of r2 commands.
|
||||
*/
|
||||
import { compile, evaluate, pragma, toJson, walkPatterns, HexpatHost, PatternError, PatternInstance, Pattern, SearchPattern } from "../lib/index";
|
||||
import { compile, evaluate, decodeType, pragma, toJson, patternToJson, dumpTree, patternColor, walkPatterns,
|
||||
HexpatHost, PatternError, PatternInstance, Pattern, SearchPattern } from "../lib/index";
|
||||
import * as A from "../lib/ast";
|
||||
|
||||
/** Host accessing r2 I/O (`p8`/`wx`), translated by a base address. */
|
||||
@@ -73,7 +74,7 @@ export class R2Host implements HexpatHost {
|
||||
const VERSION = "0.3.0";
|
||||
|
||||
const HELP = [
|
||||
"Usage: hexpat[?+-jl*espm] [args] # ImHex pattern language",
|
||||
"Usage: hexpat[?+-jl*espmt] [args] # ImHex pattern language",
|
||||
"| hexpat [-q] [-I dir] [file] evaluate file (replacing the loaded ones) and show its patterns",
|
||||
"| hexpat show the patterns of the loaded files",
|
||||
"| hexpat+ [-I dir] file evaluate file and add it to the loaded ones",
|
||||
@@ -81,11 +82,13 @@ const HELP = [
|
||||
"| hexpat-v show the r2hexpat version",
|
||||
"| hexpatj [file] show the patterns as JSON",
|
||||
"| hexpat* [file] show the patterns as r2 commands (flags, comments, data hints)",
|
||||
"| hexpat-* show r2 commands to remove generated flags (.hexpat-* applies them)",
|
||||
"| hexpatl[j] list the loaded files and their top-level patterns",
|
||||
"| hexpats [file] one-line summary (types, fields, patterns) or the error, for status bars",
|
||||
"| hexpate expr evaluate an expression or assignment in the last loaded file (writes r2 I/O)",
|
||||
"| hexpatp[?+=-ijl] ... create partial instances and masked search patterns (hexpatp? for help)",
|
||||
"| hexpatm [options] Type print a libmagic decoder for a fixed type (hexpatm? for help)",
|
||||
"| hexpatt[j*] [options] [Type [address]|pattern] view a type at an address or a loaded pattern as a tree",
|
||||
"| hexpat? show this help",
|
||||
"| options: -q (evaluate only), -I dir (add #include / import search path)",
|
||||
"| file can be 'base64:<data>' to evaluate the encoded source instead of reading a file",
|
||||
@@ -126,6 +129,19 @@ const MAGIC_HELP = [
|
||||
"| decode: pm /tmp/macho.magic @ 0 (increase block size with b if the header is larger)",
|
||||
];
|
||||
|
||||
const TREE_HELP = [
|
||||
"Usage: hexpatt[j*] [options] [Type [address]|pattern] # address-bound pattern tree",
|
||||
"| hexpatt tree of loaded patterns, with values, colors and attributes",
|
||||
"| hexpatt pattern tree of a loaded pattern (nested paths also work)",
|
||||
"| hexpatt [-f file] [-I dir] Type [address] decode a type at an r2 address (default: current seek)",
|
||||
"| hexpattj ... JSON including color, comment, displayName and attributes",
|
||||
"| hexpatt* ... r2 commands for flags, flag colors/comments and data hints",
|
||||
"| types: last loaded file or -f file; only types and constants are loaded, not placements or main()",
|
||||
"| layouts: hexpat declarations, including data-dependent arrays; addresses honor base_address",
|
||||
"| examples: hexpatt -f demo/player.hexpat Player @ 0x100; hexpatt header",
|
||||
"| apply/remove flags: .hexpatt* Player @ 0x100; .hexpat-*",
|
||||
];
|
||||
|
||||
/** Collect output lines and print them through r_cons at once (so ~grep and | work). */
|
||||
class Out {
|
||||
lines: string[] = [];
|
||||
@@ -148,11 +164,12 @@ function word(src: string): { value: string; rest: string } {
|
||||
return { value, rest: "" };
|
||||
}
|
||||
|
||||
function patternOptions(src: string): { file: string; includeDirs: string[]; alignment?: number; rest: string } {
|
||||
function patternOptions(src: string, allowAlignment = true): { file: string; includeDirs: string[]; alignment?: number; rest: string } {
|
||||
const opts = { file: "", includeDirs: [] as string[], alignment: undefined as number | undefined, rest: src.trim() };
|
||||
while (opts.rest.startsWith("-")) {
|
||||
const flag = word(opts.rest), arg = word(flag.rest);
|
||||
if (!["-f", "--file", "-I", "-a", "--align"].includes(flag.value)) throw new Error("unknown layout option '" + flag.value + "'");
|
||||
const allowed = allowAlignment ? ["-f", "--file", "-I", "-a", "--align"] : ["-f", "--file", "-I"];
|
||||
if (!allowed.includes(flag.value)) throw new Error("unknown layout option '" + flag.value + "'");
|
||||
if (!arg.value) throw new Error("missing value for " + flag.value);
|
||||
if (flag.value === "-f" || flag.value === "--file") opts.file = arg.value;
|
||||
else if (flag.value === "-I") opts.includeDirs.push(arg.value);
|
||||
@@ -249,6 +266,57 @@ function magicCommand(rest: string, out: Out): void {
|
||||
out.push(p.toMagic());
|
||||
}
|
||||
|
||||
function treeCommand(tail: string, out: Out): void {
|
||||
const mode = tail[0] && "j*?".includes(tail[0]) ? tail[0] : "";
|
||||
const rest = (mode ? tail.substring(1) : tail).trim();
|
||||
if (mode === "?" || rest === "-h" || rest === "--help") {
|
||||
for (const line of TREE_HELP) out.push(line);
|
||||
return;
|
||||
}
|
||||
const opts = patternOptions(rest, false), arg = word(opts.rest);
|
||||
let targets: { session: Session; patterns: Pattern[] }[];
|
||||
if (!arg.value) {
|
||||
if (opts.file || opts.includeDirs.length) throw new Error("missing type; see hexpatt?");
|
||||
targets = sessions.map((session) => ({ session, patterns: session.instance.patterns }));
|
||||
} else {
|
||||
const last = sessions[sessions.length - 1];
|
||||
let p: Pattern | undefined;
|
||||
if (!opts.file && !arg.rest && last) {
|
||||
p = last.instance.get(arg.value);
|
||||
if (!p && /^[a-zA-Z_]\w*[.\[]/.test(arg.value)) {
|
||||
const v = last.instance.evaluateExpression(arg.value);
|
||||
if (!(v instanceof Pattern)) throw new Error("tree requires a pattern");
|
||||
p = v;
|
||||
}
|
||||
}
|
||||
if (p) {
|
||||
if (opts.includeDirs.length) throw new Error("-I requires -f when compiling types");
|
||||
targets = [{ session: last, patterns: [p] }];
|
||||
} else {
|
||||
const program = searchProgram(opts.file, opts.includeDirs);
|
||||
const host = new R2Host(r2, programBase(program));
|
||||
host.quiet = true;
|
||||
const number = r2.cmdj("?j " + (arg.rest || "$$"));
|
||||
const address = Number(number ? number.uint64 : undefined);
|
||||
if (!Number.isSafeInteger(address) || address < 0) throw new Error("invalid address");
|
||||
const instance = decodeType(program, arg.value, host, address - host.baseAddress);
|
||||
targets = [{ session: { file: opts.file || last.file, program, instance, host }, patterns: instance.patterns }];
|
||||
}
|
||||
}
|
||||
if (mode === "j") {
|
||||
out.push(JSON.stringify(targets.flatMap(({ session: s, patterns }) =>
|
||||
patterns.filter((p) => !p.hidden).map((p) => patternToJson(s.instance, p, { base: s.host.baseAddress })))));
|
||||
} else if (mode === "*") {
|
||||
out.push("fs+hexpat");
|
||||
for (const { session, patterns } of targets) r2script(session, out, patterns);
|
||||
out.push("fs-");
|
||||
} else {
|
||||
const color = Number(r2.cmd("e scr.color").trim()) > 0;
|
||||
for (const { session: s, patterns } of targets) dumpTree(s.instance, out.push, { base: s.host.baseAddress, color, patterns });
|
||||
if (!targets.length) out.push("no patterns loaded, run 'hexpatt?' for help");
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): { quiet: boolean; includeDirs: string[]; file: string } {
|
||||
const r = { quiet: false, includeDirs: [] as string[], file: "" };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -260,6 +328,11 @@ function parseArgs(args: string[]): { quiet: boolean; includeDirs: string[]; fil
|
||||
return r;
|
||||
}
|
||||
|
||||
function programBase(program: A.Program): number {
|
||||
const ba = (pragma(program, "base_address") || "").match(/0x[0-9a-fA-F]+|\d+/);
|
||||
return ba ? parseInt(ba[0]) : R2Host.binBaseAddress(r2);
|
||||
}
|
||||
|
||||
function load(file: string, includeDirs: string[], quiet = false): Session {
|
||||
const host = new R2Host(r2);
|
||||
const inline = file.startsWith(B64);
|
||||
@@ -269,8 +342,7 @@ function load(file: string, includeDirs: string[], quiet = false): Session {
|
||||
host.includeDirs = includeDirs;
|
||||
host.quiet = quiet;
|
||||
const program = compile(src || "", host, file);
|
||||
const ba = (pragma(program, "base_address") || "").match(/0x[0-9a-fA-F]+|\d+/);
|
||||
host.baseAddress = ba ? parseInt(ba[0]) : R2Host.binBaseAddress(r2);
|
||||
host.baseAddress = programBase(program);
|
||||
return { file, program, instance: evaluate(program, host), host };
|
||||
}
|
||||
|
||||
@@ -306,13 +378,16 @@ function commentValue(inst: PatternInstance, p: Pattern): string {
|
||||
return inst.formatPattern(p, true);
|
||||
}
|
||||
|
||||
function r2script(s: Session, out: Out): void {
|
||||
function r2script(s: Session, out: Out, patterns = s.instance.patterns): void {
|
||||
const inst = s.instance;
|
||||
const base = s.host.baseAddress;
|
||||
walkPatterns(inst, (p: Pattern, path: string[]) => {
|
||||
if (p.local || p.section !== 0) return false;
|
||||
const addr = "0x" + (base + p.offset).toString(16);
|
||||
out.push("f " + flagName(path) + " " + Math.max(1, p.size) + " @ " + addr);
|
||||
const name = flagName(path), color = patternColor(p);
|
||||
out.push("f " + name + " " + Math.max(1, p.size) + " @ " + addr);
|
||||
if (color) out.push("fc " + name + "=rgb:" + color);
|
||||
if (p.comment) out.push("fC " + name + " " + sanitize(printable(p.comment, 256)));
|
||||
if (p.kind === "string" || p.kind === "wstring") {
|
||||
out.push("Cs " + p.size + " @ " + addr);
|
||||
out.push("CCu " + sanitize(p.typeName + " " + p.name + " = " + printable(String(inst.value(p)))) + " @ " + addr);
|
||||
@@ -332,7 +407,7 @@ function r2script(s: Session, out: Out): void {
|
||||
out.push("CCu " + sanitize((t ? t + " " : "") + p.name + " = " + commentValue(inst, p)) + " @ " + addr);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, patterns);
|
||||
}
|
||||
|
||||
function list(out: Out, json: boolean): void {
|
||||
@@ -410,7 +485,12 @@ function hexpatCommand(cmd: string): void {
|
||||
break;
|
||||
}
|
||||
case "-": {
|
||||
if (rest === "v") out.push(VERSION);
|
||||
if (cmd.charAt(7) === "*") {
|
||||
if (rest !== "*") throw new Error("Usage: hexpat-*");
|
||||
out.push("fs+hexpat");
|
||||
out.push("f-hexpat.*");
|
||||
out.push("fs-");
|
||||
} else if (rest === "v") out.push(VERSION);
|
||||
else if (rest === "" || rest === "*") sessions.length = 0;
|
||||
else {
|
||||
const n = parseInt(rest, 10);
|
||||
@@ -456,6 +536,7 @@ function hexpatCommand(cmd: string): void {
|
||||
}
|
||||
case "p": patternCommand(cmd.substring(7), out); break;
|
||||
case "m": magicCommand(rest, out); break;
|
||||
case "t": treeCommand(cmd.substring(7), out); break;
|
||||
default:
|
||||
for (const l of HELP) out.push(l);
|
||||
}
|
||||
@@ -479,7 +560,7 @@ function hexpatCommand(cmd: string): void {
|
||||
call: function (cmd: string) {
|
||||
if (!cmd.startsWith("hexpat")) return false;
|
||||
const c = cmd.charAt(6);
|
||||
if (c !== "" && " ?h+-j*lespm".indexOf(c) < 0) return false;
|
||||
if (c !== "" && " ?h+-j*lespmt".indexOf(c) < 0) return false;
|
||||
hexpatCommand(cmd);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- help
|
||||
Usage: hexpat[?+-jl*espm] [args] # ImHex pattern language
|
||||
Usage: hexpat[?+-jl*espmt] [args] # ImHex pattern language
|
||||
| hexpat [-q] [-I dir] [file] evaluate file (replacing the loaded ones) and show its patterns
|
||||
| hexpat show the patterns of the loaded files
|
||||
| hexpat+ [-I dir] file evaluate file and add it to the loaded ones
|
||||
@@ -7,11 +7,13 @@ Usage: hexpat[?+-jl*espm] [args] # ImHex pattern language
|
||||
| hexpat-v show the r2hexpat version
|
||||
| hexpatj [file] show the patterns as JSON
|
||||
| hexpat* [file] show the patterns as r2 commands (flags, comments, data hints)
|
||||
| hexpat-* show r2 commands to remove generated flags (.hexpat-* applies them)
|
||||
| hexpatl[j] list the loaded files and their top-level patterns
|
||||
| hexpats [file] one-line summary (types, fields, patterns) or the error, for status bars
|
||||
| hexpate expr evaluate an expression or assignment in the last loaded file (writes r2 I/O)
|
||||
| hexpatp[?+=-ijl] ... create partial instances and masked search patterns (hexpatp? for help)
|
||||
| hexpatm [options] Type print a libmagic decoder for a fixed type (hexpatm? for help)
|
||||
| hexpatt[j*] [options] [Type [address]|pattern] view a type at an address or a loaded pattern as a tree
|
||||
| hexpat? show this help
|
||||
| options: -q (evaluate only), -I dir (add #include / import search path)
|
||||
| file can be 'base64:<data>' to evaluate the encoded source instead of reading a file
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
?e -- decode types without loading a session
|
||||
hexpatt -f tests/tree_types.pat Header @ 0
|
||||
hexpattj -f tests/tree_types.pat Header 8
|
||||
hexpatlj
|
||||
?e -- current seek and address expressions
|
||||
s 8
|
||||
hexpatt -f tests/tree_types.pat Header
|
||||
f tree.address 1 @ 8
|
||||
hexpatt -f tests/tree_types.pat Header tree.address
|
||||
?e -- base address, inline sources and include directories
|
||||
omv 1 0x1000
|
||||
hexpattj -f base64:I3ByYWdtYSBiYXNlX2FkZHJlc3MgMHgxMDAwCiNpbmNsdWRlIDx0cmVlX3R5cGVzLnBhdD4K -I tests Header 0x1008
|
||||
omv 1 0
|
||||
?e -- loaded pattern trees and one-shot types preserve the session
|
||||
hexpat -q tests/syntax_structs.hexpat
|
||||
hexpatt file.head
|
||||
hexpatt file.data[0]
|
||||
hexpattj file.head.version
|
||||
hexpatt Header 4
|
||||
hexpatl
|
||||
?e -- generated flag colors and comments
|
||||
hexpatt* -f tests/tree_types.pat Header 0
|
||||
fs other
|
||||
f other.keep 1 @ 0
|
||||
.hexpatt* -f tests/tree_types.pat Header 0
|
||||
fs
|
||||
fs hexpat
|
||||
fc*
|
||||
fC hexpat.value.count
|
||||
?e -- regular flag export carries colors too
|
||||
hexpat* base64:c3RydWN0IENvbG9yIHsgdTggdGFnIFtbY29tbWVudCgiY29sb3JlZCBmaWVsZCIpXV07IH0gW1tjb2xvcigiQUJDREVGIildXTsgQ29sb3IgY29sb3JlZCBAIDA7
|
||||
.hexpat*
|
||||
fc*
|
||||
hexpate std::core::set_pattern_color(colored, 0x00332211)
|
||||
hexpatt colored
|
||||
.hexpat*
|
||||
fc*
|
||||
?e -- cleanup preview and application preserve sessions and unrelated flags
|
||||
hexpat-*
|
||||
f keep 1 @ 8
|
||||
fs other
|
||||
f hexpat.outside 1 @ 0
|
||||
.hexpat-*
|
||||
f
|
||||
fs hexpat
|
||||
f
|
||||
hexpatl
|
||||
?e -- cleanup also works after unloading
|
||||
hexpat-
|
||||
.hexpatt* -f tests/tree_types.pat Header 8
|
||||
hexpat-
|
||||
.hexpat-*
|
||||
f
|
||||
?e -- errors
|
||||
hexpatt -f tests/tree_types.pat
|
||||
hexpatt -f tests/tree_types.pat Missing 0
|
||||
hexpatt -f tests/tree_types.pat Header -1
|
||||
hexpatt -f tests/tree_types.pat Header 0x100000
|
||||
hexpatt -f tests/tree_types.pat Header no.such.address
|
||||
hexpatt Header
|
||||
@@ -0,0 +1,134 @@
|
||||
-- decode types without loading a session
|
||||
value (Header) @ 0x00000000 size=4 [[color("1144AA")]]
|
||||
|- count (u8) @ 0x00000000 = 124 (0x7c) [[color("FF0000"), comment("entry count")]]
|
||||
|- Details (Inner) @ 0x00000001 size=2 [[color("00FF00"), name("Details")]]
|
||||
| `- word (u16) @ 0x00000001 = 5415 (0x1527) [[color("00FF00")]]
|
||||
`- data (u8[1]) @ 0x00000003 size=1 [[color("1144AA")]]
|
||||
`- [0] (u8) @ 0x00000003 = 25 (0x19) [[color("1144AA")]]
|
||||
[{"name":"value","type":"Header","kind":"struct","addr":8,"offset":8,"size":5,"color":"1144AA","attributes":[{"name":"color","args":["1144AA"]}],"children":[{"name":"count","type":"u8","kind":"unsigned","addr":8,"offset":8,"size":1,"color":"FF0000","comment":"entry count","attributes":[{"name":"color","args":["FF0000"]},{"name":"comment","args":["entry count"]}],"value":131},{"name":"inner","type":"Inner","kind":"struct","addr":9,"offset":9,"size":2,"displayName":"Details","color":"00FF00","attributes":[{"name":"color","args":["00FF00"]},{"name":"name","args":["Details"]}],"children":[{"name":"word","type":"u16","kind":"unsigned","addr":9,"offset":9,"size":2,"color":"00FF00","value":48272}]},{"name":"data","type":"u8[2]","kind":"array","addr":11,"offset":11,"size":2,"color":"1144AA","count":2,"entries":[{"name":"[0]","type":"u8","kind":"unsigned","addr":11,"offset":11,"size":1,"color":"1144AA","value":164},{"name":"[1]","type":"u8","kind":"unsigned","addr":12,"offset":12,"size":1,"color":"1144AA","value":66}]}]}]
|
||||
[]
|
||||
-- current seek and address expressions
|
||||
value (Header) @ 0x00000008 size=5 [[color("1144AA")]]
|
||||
|- count (u8) @ 0x00000008 = 131 (0x83) [[color("FF0000"), comment("entry count")]]
|
||||
|- Details (Inner) @ 0x00000009 size=2 [[color("00FF00"), name("Details")]]
|
||||
| `- word (u16) @ 0x00000009 = 48272 (0xbc90) [[color("00FF00")]]
|
||||
`- data (u8[2]) @ 0x0000000b size=2 [[color("1144AA")]]
|
||||
|- [0] (u8) @ 0x0000000b = 164 (0xa4) [[color("1144AA")]]
|
||||
`- [1] (u8) @ 0x0000000c = 66 (0x42) [[color("1144AA")]]
|
||||
value (Header) @ 0x00000008 size=5 [[color("1144AA")]]
|
||||
|- count (u8) @ 0x00000008 = 131 (0x83) [[color("FF0000"), comment("entry count")]]
|
||||
|- Details (Inner) @ 0x00000009 size=2 [[color("00FF00"), name("Details")]]
|
||||
| `- word (u16) @ 0x00000009 = 48272 (0xbc90) [[color("00FF00")]]
|
||||
`- data (u8[2]) @ 0x0000000b size=2 [[color("1144AA")]]
|
||||
|- [0] (u8) @ 0x0000000b = 164 (0xa4) [[color("1144AA")]]
|
||||
`- [1] (u8) @ 0x0000000c = 66 (0x42) [[color("1144AA")]]
|
||||
-- base address, inline sources and include directories
|
||||
[{"name":"value","type":"Header","kind":"struct","addr":4104,"offset":8,"size":5,"color":"1144AA","attributes":[{"name":"color","args":["1144AA"]}],"children":[{"name":"count","type":"u8","kind":"unsigned","addr":4104,"offset":8,"size":1,"color":"FF0000","comment":"entry count","attributes":[{"name":"color","args":["FF0000"]},{"name":"comment","args":["entry count"]}],"value":131},{"name":"inner","type":"Inner","kind":"struct","addr":4105,"offset":9,"size":2,"displayName":"Details","color":"00FF00","attributes":[{"name":"color","args":["00FF00"]},{"name":"name","args":["Details"]}],"children":[{"name":"word","type":"u16","kind":"unsigned","addr":4105,"offset":9,"size":2,"color":"00FF00","value":48272}]},{"name":"data","type":"u8[2]","kind":"array","addr":4107,"offset":11,"size":2,"color":"1144AA","count":2,"entries":[{"name":"[0]","type":"u8","kind":"unsigned","addr":4107,"offset":11,"size":1,"color":"1144AA","value":164},{"name":"[1]","type":"u8","kind":"unsigned","addr":4108,"offset":12,"size":1,"color":"1144AA","value":66}]}]}]
|
||||
-- loaded pattern trees and one-shot types preserve the session
|
||||
head (Header) @ 0x00000000 size=16
|
||||
|- magic (u32) @ 0x00000000 = 421991804 (0x1927157c)
|
||||
|- version (u16) @ 0x00000004 = 62935 (0xf5d7)
|
||||
|- flags (u16) @ 0x00000006 = 51282 (0xc852)
|
||||
`- timestamp (u64) @ 0x00000008 = 13032463231746096259 (0xb4dc9c42a490bc83)
|
||||
[0] (u8) @ 0x00000015 = 126 (0x7e)
|
||||
[{"name":"version","type":"u16","kind":"unsigned","addr":4,"offset":4,"size":2,"value":62935}]
|
||||
value (Header) @ 0x00000004 size=16
|
||||
|- magic (u32) @ 0x00000004 = 3360880087 (0xc852f5d7)
|
||||
|- version (u16) @ 0x00000008 = 48259 (0xbc83)
|
||||
|- flags (u16) @ 0x0000000a = 42128 (0xa490)
|
||||
`- timestamp (u64) @ 0x0000000c = 1165859946296548418 (0x102df771b4dc9c42)
|
||||
0 tests/syntax_structs.hexpat (2 patterns)
|
||||
0x00000000 22 file (File)
|
||||
0x00000016 4 footer (u32)
|
||||
-- generated flag colors and comments
|
||||
fs+hexpat
|
||||
f hexpat.value 4 @ 0x0
|
||||
fc hexpat.value=rgb:1144aa
|
||||
f hexpat.value.count 1 @ 0x0
|
||||
fc hexpat.value.count=rgb:ff0000
|
||||
fC hexpat.value.count entry count
|
||||
Cd 1 @ 0x0
|
||||
CCu u8 count = 0x7c @ 0x0
|
||||
f hexpat.value.inner 2 @ 0x1
|
||||
fc hexpat.value.inner=rgb:00ff00
|
||||
f hexpat.value.inner.word 2 @ 0x1
|
||||
fc hexpat.value.inner.word=rgb:00ff00
|
||||
Cd 2 @ 0x1
|
||||
CCu u16 word = 0x1527 @ 0x1
|
||||
f hexpat.value.data 1 @ 0x3
|
||||
fc hexpat.value.data=rgb:1144aa
|
||||
Cd 1 1 @ 0x3
|
||||
fs-
|
||||
rgb:1144aa
|
||||
rgb:ff0000
|
||||
rgb:00ff00
|
||||
rgb:00ff00
|
||||
rgb:1144aa
|
||||
0 . classes
|
||||
0 . format
|
||||
5 . hexpat
|
||||
1 * other
|
||||
0 . sections
|
||||
0 . segments
|
||||
0 . symbols
|
||||
0 . trycatch
|
||||
'fc hexpat.value=rgb:1144aa
|
||||
'fc hexpat.value.count=rgb:ff0000
|
||||
'fc hexpat.value.inner=rgb:00ff00
|
||||
'fc hexpat.value.inner.word=rgb:00ff00
|
||||
'fc hexpat.value.data=rgb:1144aa
|
||||
entry count
|
||||
-- regular flag export carries colors too
|
||||
fs+hexpat
|
||||
f hexpat.colored 1 @ 0x0
|
||||
fc hexpat.colored=rgb:abcdef
|
||||
f hexpat.colored.tag 1 @ 0x0
|
||||
fc hexpat.colored.tag=rgb:abcdef
|
||||
fC hexpat.colored.tag colored field
|
||||
Cd 1 @ 0x0
|
||||
CCu u8 tag = 0x7c @ 0x0
|
||||
fs-
|
||||
rgb:abcdef
|
||||
rgb:abcdef
|
||||
'fc hexpat.value=rgb:1144aa
|
||||
'fc hexpat.value.count=rgb:ff0000
|
||||
'fc hexpat.colored=rgb:abcdef
|
||||
'fc hexpat.colored.tag=rgb:abcdef
|
||||
'fc hexpat.value.inner=rgb:00ff00
|
||||
'fc hexpat.value.inner.word=rgb:00ff00
|
||||
'fc hexpat.value.data=rgb:1144aa
|
||||
colored (Color) @ 0x00000000 size=1 [[color("112233")]]
|
||||
`- tag (u8) @ 0x00000000 = 124 (0x7c) [[color("112233"), comment("colored field")]]
|
||||
rgb:112233
|
||||
rgb:112233
|
||||
'fc hexpat.value=rgb:1144aa
|
||||
'fc hexpat.value.count=rgb:ff0000
|
||||
'fc hexpat.colored=rgb:112233
|
||||
'fc hexpat.colored.tag=rgb:112233
|
||||
'fc hexpat.value.inner=rgb:00ff00
|
||||
'fc hexpat.value.inner.word=rgb:00ff00
|
||||
'fc hexpat.value.data=rgb:1144aa
|
||||
-- cleanup preview and application preserve sessions and unrelated flags
|
||||
fs+hexpat
|
||||
f-hexpat.*
|
||||
fs-
|
||||
0x00000000 1 other.keep
|
||||
0x00000000 1 hexpat.outside
|
||||
0x00000008 1 keep
|
||||
0 base64 (1 patterns)
|
||||
0x00000000 1 colored (Color)
|
||||
-- cleanup also works after unloading
|
||||
rgb:1144aa
|
||||
rgb:ff0000
|
||||
rgb:00ff00
|
||||
rgb:00ff00
|
||||
rgb:1144aa
|
||||
0x00000008 1 keep
|
||||
-- errors
|
||||
ERROR: unknown symbol
|
||||
hexpat: missing type; see hexpatt?
|
||||
hexpat: error: unknown type 'Missing' (<expr>:1:1)
|
||||
hexpat: invalid address
|
||||
hexpat: error: type offset is outside the data (line 0:0)
|
||||
hexpat: invalid address
|
||||
hexpat: no types loaded: use -f file or load a pattern with hexpat
|
||||
@@ -0,0 +1,8 @@
|
||||
struct Inner { be u16 word; } [[color("00FF00")]];
|
||||
struct Header {
|
||||
u8 count [[color("FF0000"), comment("entry count")]];
|
||||
Inner inner [[name("Details")]];
|
||||
u8 data[count % 3];
|
||||
} [[color("1144AA")]];
|
||||
std::assert(false, "tree commands must not execute top-level statements");
|
||||
fn main() { std::assert(false, "tree commands must not run main()"); };
|
||||
@@ -0,0 +1,59 @@
|
||||
import { compile, decodeType, BufferHost, dumpTree, toJson, patternColor } from "../lib/index";
|
||||
|
||||
export function testTree(): void {
|
||||
const check = (ok: boolean, msg: string): void => { if (!ok) throw new Error("tree: " + msg); };
|
||||
const program = compile(`
|
||||
const u8 N = 2;
|
||||
struct Inner { u16 word; } [[color("00FF00")]];
|
||||
struct Header {
|
||||
u8 count [[color("FF0000"), comment("entry count")]];
|
||||
Inner inner [[name("Details"), tag(count)]];
|
||||
u8 data[count];
|
||||
padding[1];
|
||||
u8 secret [[hidden]];
|
||||
Inner closed [[sealed]];
|
||||
} [[color("1144AA")]];
|
||||
struct Array<T, auto Count> { T entries[Count]; };
|
||||
Header unrelated @ 10000;
|
||||
std::assert(false, "type decoder must not execute top-level statements");
|
||||
fn main() { std::assert(false, "type decoder must not call main"); };
|
||||
`);
|
||||
const data = new Uint8Array([99, 99, 2, 0x34, 0x12, 1, 2, 0, 0x78, 0x56, 3, 0]);
|
||||
const inst = decodeType(program, "Header", new BufferHost(data), 2);
|
||||
const root = inst.get("value")!;
|
||||
check(inst.patterns.length === 1 && root.offset === 2 && root.size === 9, "placed layout");
|
||||
check(inst.evaluateExpression("value.inner.word + 0") === 0x1234n, "decoded value");
|
||||
check(root.member("data")!.entryCount() === 2, "data-dependent array length");
|
||||
check(patternColor(root.member("data")!.entry(0)!) === "1144aa", "array color inheritance");
|
||||
check(patternColor(root.member("inner")!.member("word")!) === "00ff00", "member type color override");
|
||||
const json = toJson(inst, { base: 0x1000 })[0];
|
||||
check(json.addr === 0x1002 && json.color === "1144AA", "JSON address/color");
|
||||
check(json.children![0].comment === "entry count" && json.children![1].displayName === "Details", "JSON presentation metadata");
|
||||
check(json.children![1].attributes!.find((a) => a.name === "tag")!.args[0] === 2, "pattern-valued attribute serialization");
|
||||
check(json.children![2].entries![1].color === "1144AA", "JSON array entry color");
|
||||
const lines: string[] = [];
|
||||
dumpTree(inst, (line) => lines.push(line), { base: 0x1000 });
|
||||
const tree = lines.join("\n");
|
||||
check(tree.includes('value (Header) @ 0x00001002 size=9 [[color("1144AA")]]'), "root row");
|
||||
check(tree.includes('|- Details (Inner)') && tree.includes('| `- word (u16) @ 0x00001003 = 4660 (0x1234)'), "nested tree");
|
||||
check(tree.includes('tag(2)') && tree.includes('comment("entry count")'), "tree attributes");
|
||||
check(!tree.includes("secret") && !tree.includes("word (u16) @ 0x00001009"), "hidden/sealed patterns");
|
||||
check(!tree.includes("\x1b"), "plain output contains ANSI escapes");
|
||||
const limited: string[] = [];
|
||||
dumpTree(inst, (line) => limited.push(line), { patterns: [root.member("data")!], maxEntries: 1, color: true });
|
||||
check(limited[0].includes("\x1b[38;2;17;68;170m") && limited[2] === "`- ... (1 more entries)", "colored/limited tree");
|
||||
inst.evaluateExpression("std::core::set_pattern_color(value.data, 0x00efcdab)");
|
||||
check(patternColor(root.member("data")!.entry(0)!) === "abcdef", "runtime ABGR color");
|
||||
check(toJson(inst)[0].children![2].color === "abcdef", "runtime color serialization");
|
||||
check(decodeType(program, "Array<u16, N>", new BufferHost(data)).patterns[0].size === 4, "template constants");
|
||||
const conflict = decodeType(compile("const u8 value = 2; struct H { u8 bytes[value]; };"), "H", new BufferHost(data));
|
||||
check(conflict.patterns[0].name === "value_" && conflict.patterns[0].size === 2, "root collides with a constant");
|
||||
for (const offset of [-1, 1.5, data.length + 1, Number.MAX_SAFE_INTEGER + 1]) {
|
||||
let failed = false;
|
||||
try { decodeType(program, "Header", new BufferHost(data), offset); } catch (e) {
|
||||
failed = e instanceof Error && e.message.includes("outside the data");
|
||||
}
|
||||
check(failed, "invalid offset " + offset);
|
||||
}
|
||||
console.log("[test] Address-bound trees OK");
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { runHexpat, BufferHost } from "../lib/index";
|
||||
import { testSearchPatterns } from "./search_unit";
|
||||
import { testMagic } from "./magic_unit";
|
||||
import { testTree } from "./tree_unit";
|
||||
|
||||
const src = `
|
||||
struct Header {
|
||||
@@ -158,3 +159,4 @@ check(editData[0] === 11, "rejected field write modified host bytes");
|
||||
console.log("[test] Interactive writes OK");
|
||||
testSearchPatterns();
|
||||
testMagic();
|
||||
testTree();
|
||||
@@ -8,6 +8,7 @@ import { runHexpat, BufferHost } from "../lib/index";
|
||||
import { VIRTUAL_SOURCES, HOOKS } from "./upstream_hooks";
|
||||
import { testSearchPatterns } from "./search_unit";
|
||||
import { testMagic } from "./magic_unit";
|
||||
import { testTree } from "./tree_unit";
|
||||
|
||||
const size = JSON.parse(r2.cmd("ij")).core.size;
|
||||
const hex = r2.cmd("p8 " + size + " @ 0").replace(/[^0-9a-fA-F]/g, "");
|
||||
@@ -39,3 +40,4 @@ console.log("==================================");
|
||||
console.log("r2: " + passed + "/" + files.length + " passed" + (unexpected.length ? ", unexpected failures: " + unexpected.join(" ") : ""));
|
||||
try { testSearchPatterns(); } catch (e) { console.log("FAIL partial search patterns: " + String(e)); }
|
||||
try { testMagic(); } catch (e) { console.log("FAIL magic export: " + String(e)); }
|
||||
try { testTree(); } catch (e) { console.log("FAIL address-bound trees: " + String(e)); }
|
||||
Reference in new issue
Block a user