romdevtools 0.43.0 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +322 -0
- package/examples/n64/platformer/main.c +158 -0
- package/examples/n64/puzzle/main.c +117 -0
- package/examples/n64/racing/main.c +147 -0
- package/examples/n64/shmup/main.c +122 -0
- package/examples/n64/sports/main.c +127 -0
- package/examples/ps1/platformer/main.c +158 -0
- package/examples/ps1/puzzle/main.c +125 -0
- package/examples/ps1/racing/main.c +147 -0
- package/examples/ps1/shmup/main.c +192 -0
- package/examples/ps1/sports/main.c +127 -0
- package/examples/ps1/sprite_move/main.c +38 -0
- package/package.json +9 -2
- package/src/analysis/analyze.js +169 -29
- package/src/analysis/decompile.js +4 -0
- package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
- package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
- package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
- package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
- package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
- package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
- package/src/analysis/rizin.js +22 -1
- package/src/cores/capabilities.js +90 -2
- package/src/cores/registry.js +12 -0
- package/src/host/LibretroGL.js +270 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +144 -3
- package/src/host/callbacks.js +18 -2
- package/src/host/coreLoader.js +49 -2
- package/src/host/cpu-state.js +27 -0
- package/src/host/framebuffer.js +14 -1
- package/src/host/glOptionalDep.js +60 -0
- package/src/host/n64-ai-state.js +43 -0
- package/src/host/ps1-spu-state.js +65 -0
- package/src/host/retroConstants.js +14 -0
- package/src/mcp/tools/cheats.js +73 -4
- package/src/mcp/tools/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- package/src/mcp/tools/index.js +12 -2
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +20 -4
- package/src/mcp/tools/platforms.js +38 -4
- package/src/mcp/tools/project.js +100 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- package/src/mcp/tools/watch-memory.js +93 -20
- package/src/platforms/n64/lib/c/n64.c +196 -0
- package/src/platforms/n64/lib/c/n64.h +68 -0
- package/src/platforms/ps1/lib/c/psx.c +200 -0
- package/src/platforms/ps1/lib/c/psx.h +83 -0
- package/src/toolchains/cc65/cc65.js +11 -0
- package/src/toolchains/index.js +35 -0
- package/src/toolchains/mips-c/lib/be/libc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libm.a +0 -0
- package/src/toolchains/mips-c/lib/el/libc.a +0 -0
- package/src/toolchains/mips-c/lib/el/libm.a +0 -0
- package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
- package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
- package/src/toolchains/mips-c/lib/n64.ld +15 -0
- package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
- package/src/toolchains/mips-c/lib/ps1.ld +15 -0
- package/src/toolchains/mips-c/lib/softint.c +37 -0
- package/src/toolchains/mips-c/mips-c.js +155 -0
- package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
package/src/analysis/analyze.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// resolved by the existing disasm mappers — analysis here is whole-file.
|
|
13
13
|
import { readFile } from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { runRizinJson, RIZIN_ARCH } from "./rizin.js";
|
|
15
|
+
import { runRizinJson, RIZIN_ARCH, RIZIN_ENDIAN } from "./rizin.js";
|
|
16
16
|
import { decompileFunction, SLEIGH_LANGID } from "./decompile.js";
|
|
17
17
|
import { registersForPlatform } from "../platforms/common/registers.js";
|
|
18
18
|
|
|
@@ -133,13 +133,72 @@ export function sniffPlatform(p) {
|
|
|
133
133
|
if (/\.(lnx|lyx)$/i.test(p)) return "lynx";
|
|
134
134
|
if (/\.gba$/i.test(p)) return "gba";
|
|
135
135
|
if (/\.pce$/i.test(p)) return "pce";
|
|
136
|
+
if (/\.(z64|n64|v64)$/i.test(p)) return "n64";
|
|
137
|
+
if (/\.(psexe|psx)$/i.test(p)) return "ps1"; // .exe/.bin ambiguous — pass platform explicitly
|
|
136
138
|
if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
|
|
137
139
|
return null;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/** rizin asm.bits per arch (analysis defaults; rizin's loader usually sets
|
|
141
143
|
* these for recognized formats, but raw blobs need a hint). */
|
|
142
|
-
const BITS = { arm: 32, m68k: 32, snes: 16 };
|
|
144
|
+
const BITS = { arm: 32, m68k: 32, snes: 16, mips: 32 };
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The rizin analysis-SEED command for a context. For the 8/16-bit platforms
|
|
148
|
+
* rizin's `aaa` recognizes the bin format / vectors and seeds itself. For a RAW
|
|
149
|
+
* MIPS image (N64/PS1) there's no recognized entry, so `aaa` finds nothing —
|
|
150
|
+
* instead define a function at the code start and recursively analyze its call
|
|
151
|
+
* graph (`af` + `aac`), which surfaces the real function tree (verified: 251
|
|
152
|
+
* functions on a real libdragon N64 ROM where `aaa` found 0).
|
|
153
|
+
* @param {{arch:string, codeStart:number}} ctx
|
|
154
|
+
* @returns {string} the seed command (no trailing semicolon)
|
|
155
|
+
*/
|
|
156
|
+
function analysisSeed({ arch, codeStart }) {
|
|
157
|
+
if (arch === "mips") {
|
|
158
|
+
const at = "0x" + (codeStart || 0).toString(16);
|
|
159
|
+
return `af @ ${at}; aac @ ${at}`;
|
|
160
|
+
}
|
|
161
|
+
return "aaa";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** MIPS analysis runs FLAT (rizin ignores -B on a raw malloc:// buffer), so rizin
|
|
165
|
+
* reports FILE-OFFSET addresses. For PS1 loadContext left-pads the .text so flat
|
|
166
|
+
* offset == the VA's low 20 bits (see there); we add the VA's HIGH bits back as
|
|
167
|
+
* `rebase` so callers get real VAs that round-trip through the decompile
|
|
168
|
+
* VA→fileOffset math. N64 (.z64) keeps its absolute VAs baked into the code and is
|
|
169
|
+
* analyzed flat from codeStart, so no rebase. */
|
|
170
|
+
function mipsAnalysisBase({ arch, platform, loadBase, codeStart }) {
|
|
171
|
+
const rebase = (arch === "mips" && platform === "ps1" && loadBase) ? (loadBase & 0xfff00000) >>> 0 : 0;
|
|
172
|
+
return { baddr: undefined, seedAt: codeStart, rebase };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Normalize an N64 ROM to BIG-ENDIAN (.z64) byte order. Dumps come in three
|
|
177
|
+
* orders, distinguished by the first 4 bytes of the header:
|
|
178
|
+
* .z64 (big) 80 37 12 40 — native; no change
|
|
179
|
+
* .v64 (byteswap)37 80 40 12 — swap every 2 bytes
|
|
180
|
+
* .n64 (little) 40 12 37 80 — swap every 4 bytes
|
|
181
|
+
* MIPS analysis wants the big-endian image so addresses + instruction words line up.
|
|
182
|
+
* @param {Uint8Array} bytes
|
|
183
|
+
* @returns {{ bytes: Uint8Array, reordered: string|null }}
|
|
184
|
+
*/
|
|
185
|
+
function normalizeN64ByteOrder(bytes) {
|
|
186
|
+
if (bytes.length < 4) return { bytes, reordered: null };
|
|
187
|
+
const b = bytes;
|
|
188
|
+
const is = (a, c, d, e) => b[0] === a && b[1] === c && b[2] === d && b[3] === e;
|
|
189
|
+
if (is(0x80, 0x37, 0x12, 0x40)) return { bytes, reordered: null }; // .z64 big — native
|
|
190
|
+
if (is(0x37, 0x80, 0x40, 0x12)) { // .v64 byteswapped — swap pairs
|
|
191
|
+
const out = new Uint8Array(b.length & ~1);
|
|
192
|
+
for (let i = 0; i + 1 < b.length; i += 2) { out[i] = b[i + 1]; out[i + 1] = b[i]; }
|
|
193
|
+
return { bytes: out, reordered: "v64 (byteswapped) → z64" };
|
|
194
|
+
}
|
|
195
|
+
if (is(0x40, 0x12, 0x37, 0x80)) { // .n64 little — swap dwords
|
|
196
|
+
const out = new Uint8Array(b.length & ~3);
|
|
197
|
+
for (let i = 0; i + 3 < b.length; i += 4) { out[i] = b[i + 3]; out[i + 1] = b[i + 2]; out[i + 2] = b[i + 1]; out[i + 3] = b[i]; }
|
|
198
|
+
return { bytes: out, reordered: "n64 (little-endian dwords) → z64" };
|
|
199
|
+
}
|
|
200
|
+
return { bytes, reordered: null }; // unknown header — leave as-is
|
|
201
|
+
}
|
|
143
202
|
|
|
144
203
|
/** Build the common rizin invocation context for a ROM + platform. Returns
|
|
145
204
|
* { romBytes, arch, bits, note } — arch null means let rizin sniff. */
|
|
@@ -175,11 +234,53 @@ async function loadContext(romPath, platformOverride) {
|
|
|
175
234
|
loadBase = romBytes[0] | (romBytes[1] << 8);
|
|
176
235
|
romBytes = romBytes.subarray(2);
|
|
177
236
|
}
|
|
237
|
+
const warningsEarly = [];
|
|
238
|
+
// N64: normalize to big-endian (.z64) byte order, then the ROM header's entry
|
|
239
|
+
// point (big-endian word at 0x08) is the CPU load base. The 0x1000-byte header
|
|
240
|
+
// (IPL3 bootcode) precedes the game; rizin analyzes the whole image but the
|
|
241
|
+
// entry tells functions where code starts.
|
|
242
|
+
let codeStart = 0; // file offset where executable code begins (post-header)
|
|
243
|
+
if (platform === "n64") {
|
|
244
|
+
const norm = normalizeN64ByteOrder(romBytes);
|
|
245
|
+
romBytes = norm.bytes;
|
|
246
|
+
if (norm.reordered) warningsEarly.push(`N64 ROM was ${norm.reordered} byte order — normalized to z64 (big-endian) for analysis.`);
|
|
247
|
+
if (romBytes.length >= 0x0c) {
|
|
248
|
+
// entry point: big-endian 32-bit at offset 0x08.
|
|
249
|
+
loadBase = (romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b];
|
|
250
|
+
}
|
|
251
|
+
// The IPL3 bootcode occupies the first 0x1000 bytes; game code starts after it.
|
|
252
|
+
// rizin's `aaa` doesn't recognize a raw N64 ROM's entry, so analysis is seeded
|
|
253
|
+
// here (see ANALYSIS_SEED).
|
|
254
|
+
codeStart = 0x1000;
|
|
255
|
+
}
|
|
256
|
+
// PS1 PS-EXE: a 2048-byte header; the load (t_addr) is a little-endian word at
|
|
257
|
+
// 0x18, and the code follows the header. Strip the header so rizin sees code at
|
|
258
|
+
// the load address. A raw .bin (no PS-EXE magic) is left flat.
|
|
259
|
+
if (platform === "ps1" && romBytes.length >= 0x800 &&
|
|
260
|
+
romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) { // "PS-X"
|
|
261
|
+
loadBase = (romBytes[0x18] | (romBytes[0x19] << 8) | (romBytes[0x1a] << 16) | (romBytes[0x1b] << 24)) >>> 0;
|
|
262
|
+
const text = romBytes.subarray(0x800);
|
|
263
|
+
// rizin ignores -B on a raw malloc:// buffer, so it addresses flat from 0. PS1
|
|
264
|
+
// jal targets are ABSOLUTE VAs (e.g. jal 0x80010518) — to let rizin follow them,
|
|
265
|
+
// left-pad the .text by the load address's low 20 bits so flat offset N == the
|
|
266
|
+
// VA's low bits (jal masks to a 28-bit region). The high bits (0x80000000) are
|
|
267
|
+
// added back as `rebase`. Without this, every cross-function call dangles and
|
|
268
|
+
// only the entry blob is discovered. (Cap the pad so a weird t_addr can't OOM.)
|
|
269
|
+
const lowPad = loadBase & 0x000fffff;
|
|
270
|
+
if (lowPad > 0 && lowPad <= 0x200000) {
|
|
271
|
+
const padded = new Uint8Array(lowPad + text.length);
|
|
272
|
+
padded.set(text, lowPad);
|
|
273
|
+
romBytes = padded;
|
|
274
|
+
codeStart = lowPad;
|
|
275
|
+
} else {
|
|
276
|
+
romBytes = text;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
178
279
|
|
|
179
280
|
// A6: container/format sniff. Some dumps are interleaved/headered such that a
|
|
180
281
|
// FLAT read scrambles every byte → fake "bad instruction" noise everywhere.
|
|
181
282
|
// Detect + auto-correct, and warn so a flat disasm isn't silently wrong.
|
|
182
|
-
const warnings = [];
|
|
283
|
+
const warnings = [...warningsEarly];
|
|
183
284
|
if (platform === "genesis") {
|
|
184
285
|
const smd = deinterleaveSmd(romBytes);
|
|
185
286
|
if (smd) {
|
|
@@ -188,7 +289,7 @@ async function loadContext(romPath, platformOverride) {
|
|
|
188
289
|
"auto-deinterleaved before analysis. A flat read of the original would scramble every instruction.");
|
|
189
290
|
}
|
|
190
291
|
}
|
|
191
|
-
return { platform, romBytes, arch, bits: BITS[arch], approx, loadBase, warnings };
|
|
292
|
+
return { platform, romBytes, arch, bits: BITS[arch], endian: RIZIN_ENDIAN[platform], approx, loadBase, codeStart, warnings };
|
|
192
293
|
}
|
|
193
294
|
|
|
194
295
|
/** Detect + reverse Sega Mega Drive SMD interleaving. An .smd dump is a 512-byte
|
|
@@ -227,11 +328,12 @@ function hx(n) { return "0x" + (n >>> 0).toString(16); }
|
|
|
227
328
|
* @returns {{platform, count, functions: Array<{address, name, size, nbbs, cc, callers, callees}>}}
|
|
228
329
|
*/
|
|
229
330
|
export async function analyzeFunctions(romPath, platformOverride) {
|
|
230
|
-
const { platform, romBytes, arch, bits, loadBase, warnings } = await loadContext(romPath, platformOverride);
|
|
231
|
-
const
|
|
331
|
+
const { platform, romBytes, arch, bits, endian, loadBase, codeStart, warnings } = await loadContext(romPath, platformOverride);
|
|
332
|
+
const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
|
|
333
|
+
const fns = await runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${analysisSeed({ arch, codeStart: seedAt })}; aflj` });
|
|
232
334
|
const functions = fns.map((f) => ({
|
|
233
|
-
address: f.offset,
|
|
234
|
-
addressHex: hx(f.offset),
|
|
335
|
+
address: (f.offset + rebase) >>> 0,
|
|
336
|
+
addressHex: hx((f.offset + rebase) >>> 0),
|
|
235
337
|
name: f.name,
|
|
236
338
|
size: f.size,
|
|
237
339
|
nbbs: f.nbbs, // basic-block count
|
|
@@ -269,30 +371,35 @@ export async function analyzeFunctions(romPath, platformOverride) {
|
|
|
269
371
|
*/
|
|
270
372
|
export async function analyzeCfg(romPath, address, platformOverride) {
|
|
271
373
|
if (address == null) throw new Error("analyze cfg: address required");
|
|
272
|
-
const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
|
|
374
|
+
const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
|
|
273
375
|
// afbj = basic blocks of the function as JSON: each block has addr/size/jump/
|
|
274
376
|
// fail/ninstr. `jump` is the taken edge; `fail` (present only on conditional
|
|
275
377
|
// blocks) is the fall-through. This is the structured CFG source — `agf json`
|
|
276
378
|
// only gives a text body blob with untyped out_nodes.
|
|
379
|
+
// MIPS: seed the call graph first (the address is a vaddr from `functions`,
|
|
380
|
+
// which rizin recovers during `aac`). PS1 rebases to loadBase so the vaddr lines up.
|
|
381
|
+
const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
|
|
382
|
+
const flat = ((address >>> 0) - rebase) >>> 0; // VA → flat offset rizin uses
|
|
383
|
+
const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
|
|
277
384
|
const blocks = await runRizinJson({
|
|
278
|
-
romBytes, arch, bits, baddr
|
|
279
|
-
commands:
|
|
385
|
+
romBytes, arch, bits, endian, baddr,
|
|
386
|
+
commands: `${analysisSeed({ arch, codeStart: seedAt })}; af @ ${hx(flat)}; afbj @ ${hx(flat)}`,
|
|
280
387
|
});
|
|
281
388
|
if (!Array.isArray(blocks) || blocks.length === 0) {
|
|
282
389
|
return { platform, arch, address, addressHex: hx(address), nodes: [], edges: [], note: "no function/blocks at address" };
|
|
283
390
|
}
|
|
284
391
|
const nodes = blocks.map((b) => ({
|
|
285
|
-
id: b.addr,
|
|
286
|
-
address: b.addr,
|
|
287
|
-
addressHex: hx(b.addr),
|
|
392
|
+
id: rb(b.addr),
|
|
393
|
+
address: rb(b.addr),
|
|
394
|
+
addressHex: hx(rb(b.addr)),
|
|
288
395
|
size: b.size,
|
|
289
396
|
ninstr: b.ninstr,
|
|
290
397
|
}));
|
|
291
398
|
const edges = [];
|
|
292
399
|
for (const b of blocks) {
|
|
293
400
|
const conditional = b.fail != null;
|
|
294
|
-
if (b.jump != null) edges.push({ from: b.addr, to: b.jump, type: conditional ? "branch_true" : "jump_or_fall" });
|
|
295
|
-
if (b.fail != null) edges.push({ from: b.addr, to: b.fail, type: "branch_false" });
|
|
401
|
+
if (b.jump != null) edges.push({ from: rb(b.addr), to: rb(b.jump), type: conditional ? "branch_true" : "jump_or_fall" });
|
|
402
|
+
if (b.fail != null) edges.push({ from: rb(b.addr), to: rb(b.fail), type: "branch_false" });
|
|
296
403
|
}
|
|
297
404
|
return {
|
|
298
405
|
platform, arch,
|
|
@@ -308,10 +415,13 @@ export async function analyzeCfg(romPath, address, platformOverride) {
|
|
|
308
415
|
*/
|
|
309
416
|
export async function analyzeXrefs(romPath, address, platformOverride) {
|
|
310
417
|
if (address == null) throw new Error("analyze xrefs: address required");
|
|
311
|
-
const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
|
|
418
|
+
const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
|
|
419
|
+
const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
|
|
420
|
+
const flat = ((address >>> 0) - rebase) >>> 0;
|
|
421
|
+
const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
|
|
312
422
|
let refs;
|
|
313
423
|
try {
|
|
314
|
-
refs = await runRizinJson({ romBytes, arch, bits, baddr:
|
|
424
|
+
refs = await runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${analysisSeed({ arch, codeStart: seedAt })}; axtj @ ${hx(flat)}` });
|
|
315
425
|
} catch (e) {
|
|
316
426
|
// axtj prints nothing (not even `[]`) when there are zero refs → our JSON
|
|
317
427
|
// guard throws. Treat "no JSON" as "no refs".
|
|
@@ -319,9 +429,9 @@ export async function analyzeXrefs(romPath, address, platformOverride) {
|
|
|
319
429
|
else throw e;
|
|
320
430
|
}
|
|
321
431
|
const out = (refs ?? []).map((r) => ({
|
|
322
|
-
from: r.from,
|
|
323
|
-
fromHex: hx(r.from),
|
|
324
|
-
to: r.to,
|
|
432
|
+
from: rb(r.from),
|
|
433
|
+
fromHex: hx(rb(r.from)),
|
|
434
|
+
to: rb(r.to),
|
|
325
435
|
type: (r.type || "").toLowerCase(), // CALL / CODE / DATA / STRING
|
|
326
436
|
opcode: r.opcode,
|
|
327
437
|
}));
|
|
@@ -333,23 +443,25 @@ export async function analyzeXrefs(romPath, address, platformOverride) {
|
|
|
333
443
|
* analysis pass. The "give me the shape of this ROM" call.
|
|
334
444
|
*/
|
|
335
445
|
export async function analyzeStructure(romPath, platformOverride) {
|
|
336
|
-
const { platform, romBytes, arch, bits, loadBase } = await loadContext(romPath, platformOverride);
|
|
337
|
-
const baddr = loadBase
|
|
446
|
+
const { platform, romBytes, arch, bits, endian, loadBase, codeStart } = await loadContext(romPath, platformOverride);
|
|
447
|
+
const { baddr, seedAt, rebase } = mipsAnalysisBase({ arch, platform, loadBase, codeStart });
|
|
448
|
+
const seed = analysisSeed({ arch, codeStart: seedAt });
|
|
338
449
|
const [fns, strings, entries] = await Promise.all([
|
|
339
|
-
runRizinJson({ romBytes, arch, bits, baddr, commands:
|
|
340
|
-
runRizinJson({ romBytes, arch, bits, baddr, commands:
|
|
341
|
-
runRizinJson({ romBytes, arch, bits, baddr, commands:
|
|
450
|
+
runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${seed}; aflj` }).catch(() => []),
|
|
451
|
+
runRizinJson({ romBytes, arch, bits, endian, baddr, commands: `${seed}; izj` }).catch(() => []),
|
|
452
|
+
runRizinJson({ romBytes, arch, bits, baddr, commands: `${seed}; iej` }).catch(() => []),
|
|
342
453
|
]);
|
|
454
|
+
const rb = (a) => (a == null ? a : (a + rebase) >>> 0);
|
|
343
455
|
return {
|
|
344
456
|
platform, arch,
|
|
345
457
|
functionCount: Array.isArray(fns) ? fns.length : 0,
|
|
346
458
|
stringCount: Array.isArray(strings) ? strings.length : 0,
|
|
347
|
-
entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: e.vaddr, addressHex: hx(e.vaddr) })),
|
|
459
|
+
entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: rb(e.vaddr), addressHex: hx(rb(e.vaddr)) })),
|
|
348
460
|
functions: (Array.isArray(fns) ? fns : []).slice(0, 512).map((f) => ({
|
|
349
|
-
address: f.offset, addressHex: hx(f.offset), name: f.name, size: f.size, callers: f.indegree ?? 0,
|
|
461
|
+
address: rb(f.offset), addressHex: hx(rb(f.offset)), name: f.name, size: f.size, callers: f.indegree ?? 0,
|
|
350
462
|
})),
|
|
351
463
|
strings: (Array.isArray(strings) ? strings : []).slice(0, 256).map((s) => ({
|
|
352
|
-
address: s.vaddr, addressHex: hx(s.vaddr), value: s.string,
|
|
464
|
+
address: rb(s.vaddr), addressHex: hx(rb(s.vaddr)), value: s.string,
|
|
353
465
|
})),
|
|
354
466
|
};
|
|
355
467
|
}
|
|
@@ -507,6 +619,34 @@ export async function analyzeDecompile(romPath, address, platformOverride) {
|
|
|
507
619
|
// to pure garbage.
|
|
508
620
|
if (platform === "genesis") romBytes = deinterleaveSmd(romBytes) ?? romBytes;
|
|
509
621
|
|
|
622
|
+
// MIPS (N64/PS1): `address` is a vaddr from target='functions'. Map it to a file
|
|
623
|
+
// offset and decompile via the MIPS SLEIGH spec (MIPS:BE:32 for N64, MIPS:LE:32
|
|
624
|
+
// for PS1). N64: normalize byte order, fileOff = vaddr - entryVaddr + 0x1000 (post
|
|
625
|
+
// IPL3). PS1: strip PS-EXE, fileOff = vaddr - loadAddr.
|
|
626
|
+
if (platform === "n64" || platform === "ps1") {
|
|
627
|
+
let fileOff;
|
|
628
|
+
if (platform === "n64") {
|
|
629
|
+
romBytes = normalizeN64ByteOrder(romBytes).bytes;
|
|
630
|
+
const entry = ((romBytes[0x08] << 24) | (romBytes[0x09] << 16) | (romBytes[0x0a] << 8) | romBytes[0x0b]) >>> 0;
|
|
631
|
+
fileOff = ((address >>> 0) - entry + 0x1000) >>> 0;
|
|
632
|
+
} else {
|
|
633
|
+
let loadAddr = 0;
|
|
634
|
+
if (romBytes.length >= 0x800 && romBytes[0] === 0x50 && romBytes[1] === 0x53 && romBytes[2] === 0x2d && romBytes[3] === 0x58) {
|
|
635
|
+
loadAddr = (romBytes[0x18] | (romBytes[0x19] << 8) | (romBytes[0x1a] << 16) | (romBytes[0x1b] << 24)) >>> 0;
|
|
636
|
+
romBytes = romBytes.subarray(0x800);
|
|
637
|
+
}
|
|
638
|
+
fileOff = ((address >>> 0) - loadAddr) >>> 0;
|
|
639
|
+
}
|
|
640
|
+
if (fileOff >= romBytes.length) {
|
|
641
|
+
throw new Error(`decompile: ${platform} address ${hx(address)} maps to file offset ${hx(fileOff)}, outside the ${romBytes.length}-byte image. Use an address from target='functions'.`);
|
|
642
|
+
}
|
|
643
|
+
const rm = await decompileFunction({ platform, romBytes, fileOffset: fileOff });
|
|
644
|
+
return {
|
|
645
|
+
platform, langid: rm.langid, address, addressHex: hx(address),
|
|
646
|
+
code: prettyDecompile(rm.code, platform), warnings: rm.warnings,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
510
650
|
// SNES: banked 24-bit space. `address` is a LoROM/HiROM CPU address (what
|
|
511
651
|
// target='functions'/'cfg' report). Lay the cart out by CPU address so BOTH
|
|
512
652
|
// the function address AND its in-bank/JSL operands resolve, then decompile at
|
|
@@ -47,6 +47,10 @@ export const SLEIGH_LANGID = {
|
|
|
47
47
|
genesis: "68000:BE:32:default",
|
|
48
48
|
snes: "65816:LE:24:snes",
|
|
49
49
|
pce: "HuC6280:LE:16:default",
|
|
50
|
+
// 32-bit MIPS tier. PS1 R3000 = little-endian; N64 R4300 = big-endian (games run
|
|
51
|
+
// 32-bit MIPS III code). Ghidra ships both MIPS variants in its stock SLEIGH.
|
|
52
|
+
ps1: "MIPS:LE:32:default",
|
|
53
|
+
n64: "MIPS:BE:32:default",
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
/**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<language_definitions>
|
|
3
|
+
<language processor="MIPS"
|
|
4
|
+
endian="big"
|
|
5
|
+
size="32"
|
|
6
|
+
variant="default"
|
|
7
|
+
version="1.7"
|
|
8
|
+
slafile="mips32be.sla"
|
|
9
|
+
processorspec="mips32.pspec"
|
|
10
|
+
id="MIPS:BE:32:default">
|
|
11
|
+
<description>MIPS32 big endian (N64 R4300, 32-bit code)</description>
|
|
12
|
+
<compiler name="default" spec="mips32be.cspec" id="default"/>
|
|
13
|
+
</language>
|
|
14
|
+
<language processor="MIPS"
|
|
15
|
+
endian="little"
|
|
16
|
+
size="32"
|
|
17
|
+
variant="default"
|
|
18
|
+
version="1.7"
|
|
19
|
+
slafile="mips32le.sla"
|
|
20
|
+
processorspec="mips32.pspec"
|
|
21
|
+
id="MIPS:LE:32:default">
|
|
22
|
+
<description>MIPS32 little endian (PS1 R3000)</description>
|
|
23
|
+
<compiler name="default" spec="mips32le.cspec" id="default"/>
|
|
24
|
+
</language>
|
|
25
|
+
</language_definitions>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
|
|
3
|
+
<processor_spec>
|
|
4
|
+
<properties>
|
|
5
|
+
<property key="addressesDoNotAppearDirectlyInCode" value="true"/>
|
|
6
|
+
<property key="emulateInstructionStateModifierClass" value="ghidra.program.emulation.MIPSEmulateInstructionStateModifier"/>
|
|
7
|
+
<property key="assemblyRating:MIPS:BE:32:default" value="PLATINUM"/>
|
|
8
|
+
</properties>
|
|
9
|
+
<programcounter register="pc"/>
|
|
10
|
+
<context_data>
|
|
11
|
+
<context_set space="ram">
|
|
12
|
+
<set name="PAIR_INSTRUCTION_FLAG" val="0" description="1 if LWL/LWR instruction is a pair"/>
|
|
13
|
+
<set name="RELP" val="1" description="1 if mips16e, 0 if micromips"/>
|
|
14
|
+
</context_set>
|
|
15
|
+
</context_data>
|
|
16
|
+
<register_data>
|
|
17
|
+
<register name="contextreg" hidden="true"/>
|
|
18
|
+
<register name="ext_isjal" hidden="true"/>
|
|
19
|
+
<register name="ext_value" hidden="true"/>
|
|
20
|
+
<register name="ext_value_select" hidden="true"/>
|
|
21
|
+
<register name="ext_value_1005" hidden="true"/>
|
|
22
|
+
<register name="ext_value_1004" hidden="true"/>
|
|
23
|
+
<register name="ext_value_sa40" hidden="true"/>
|
|
24
|
+
<register name="ext_value_xreg" hidden="true"/>
|
|
25
|
+
<register name="ext_value_frame" hidden="true"/>
|
|
26
|
+
<register name="ext_value_areg" hidden="true"/>
|
|
27
|
+
<register name="ext_value_b0" hidden="true"/>
|
|
28
|
+
<register name="ext_value_b1" hidden="true"/>
|
|
29
|
+
<register name="ext_value_b2" hidden="true"/>
|
|
30
|
+
<register name="ext_value_b3" hidden="true"/>
|
|
31
|
+
<register name="ext_value_saz" hidden="true"/>
|
|
32
|
+
<register name="ext_value_1511" hidden="true"/>
|
|
33
|
+
<register name="ext_value_1511s" hidden="true"/>
|
|
34
|
+
<register name="ext_value_1411" hidden="true"/>
|
|
35
|
+
<register name="ext_value_1411s" hidden="true"/>
|
|
36
|
+
<register name="ext_tgt_2521" hidden="true"/>
|
|
37
|
+
<register name="ext_tgt_2016" hidden="true"/>
|
|
38
|
+
<register name="ext_is_ext" hidden="true"/>
|
|
39
|
+
<register name="ext_m16r32" hidden="true"/>
|
|
40
|
+
<register name="ext_m16r32a" hidden="true"/>
|
|
41
|
+
<register name="ext_reg_high" hidden="true"/>
|
|
42
|
+
<register name="ext_reg_low" hidden="true"/>
|
|
43
|
+
<register name="ext_svrs_xs" hidden="true"/>
|
|
44
|
+
<register name="ext_svrs_s1" hidden="true"/>
|
|
45
|
+
<register name="ext_svrs_s0" hidden="true"/>
|
|
46
|
+
<register name="ext_tgt_x" hidden="true"/>
|
|
47
|
+
<register name="ext_done" hidden="true"/>
|
|
48
|
+
<register name="ext_delay" hidden="true"/>
|
|
49
|
+
<register name="REL6" hidden="true"/>
|
|
50
|
+
<register name="RELP" hidden="true"/>
|
|
51
|
+
<register name="ext_t4" hidden="true"/>
|
|
52
|
+
<register name="ext_tra" hidden="true"/>
|
|
53
|
+
<register name="ext_32_code" hidden="true"/>
|
|
54
|
+
<register name="ext_32_codes" hidden="true"/>
|
|
55
|
+
<register name="ext_32_addim" hidden="true"/>
|
|
56
|
+
<register name="ext_32_addims" hidden="true"/>
|
|
57
|
+
<register name="ext_32_imm2" hidden="true"/>
|
|
58
|
+
<register name="ext_32_imm2s" hidden="true"/>
|
|
59
|
+
<register name="ext_32_imm3" hidden="true"/>
|
|
60
|
+
<register name="ext_32_imm3s" hidden="true"/>
|
|
61
|
+
<register name="ext_32_imm5" hidden="true"/>
|
|
62
|
+
<register name="ext_32_imm5s" hidden="true"/>
|
|
63
|
+
<register name="ext_32_imm6" hidden="true"/>
|
|
64
|
+
<register name="ext_32_rlist" hidden="true"/>
|
|
65
|
+
<register name="ext_32_base" hidden="true"/>
|
|
66
|
+
<register name="ext_32_basea" hidden="true"/>
|
|
67
|
+
<register name="ext_32_rd" hidden="true"/>
|
|
68
|
+
<register name="ext_32_rdset" hidden="true"/>
|
|
69
|
+
<register name="ext_32_rs1" hidden="true"/>
|
|
70
|
+
<register name="ext_32_rs1lo" hidden="true"/>
|
|
71
|
+
<register name="ext_32_rs1set" hidden="true"/>
|
|
72
|
+
<register name="ext_16_rs" hidden="true"/>
|
|
73
|
+
<register name="ext_16_rslo" hidden="true"/>
|
|
74
|
+
<register name="ext_16_rshi" hidden="true"/>
|
|
75
|
+
<register name="ext_off16_s" hidden="true"/>
|
|
76
|
+
<register name="ext_off16_u" hidden="true"/>
|
|
77
|
+
</register_data>
|
|
78
|
+
</processor_spec>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
|
|
3
|
+
<compiler_spec>
|
|
4
|
+
<data_organization>
|
|
5
|
+
<pointer_size value="4"/>
|
|
6
|
+
<float_size value="4" />
|
|
7
|
+
<double_size value="8" />
|
|
8
|
+
<long_double_size value="8" />
|
|
9
|
+
<size_alignment_map>
|
|
10
|
+
<entry size="1" alignment="1" />
|
|
11
|
+
<entry size="2" alignment="2" />
|
|
12
|
+
<entry size="4" alignment="4" />
|
|
13
|
+
<entry size="8" alignment="8" />
|
|
14
|
+
</size_alignment_map>
|
|
15
|
+
</data_organization>
|
|
16
|
+
|
|
17
|
+
<stackpointer register="sp" space="ram"/>
|
|
18
|
+
<funcptr align="2"/>
|
|
19
|
+
<global>
|
|
20
|
+
<range space="ram"/>
|
|
21
|
+
<range space="register" first="0x2000" last="0x2fff"/>
|
|
22
|
+
</global>
|
|
23
|
+
<returnaddress>
|
|
24
|
+
<register name="ra"/>
|
|
25
|
+
</returnaddress>
|
|
26
|
+
<default_proto>
|
|
27
|
+
<prototype name="__stdcall" extrapop="0" stackshift="0">
|
|
28
|
+
<input>
|
|
29
|
+
<pentry minsize="1" maxsize="8" metatype="float">
|
|
30
|
+
<register name="f12_13"/>
|
|
31
|
+
</pentry>
|
|
32
|
+
<pentry minsize="1" maxsize="8" metatype="float">
|
|
33
|
+
<register name="f14_15"/>
|
|
34
|
+
</pentry>
|
|
35
|
+
<pentry minsize="1" maxsize="4">
|
|
36
|
+
<register name="a0"/>
|
|
37
|
+
</pentry>
|
|
38
|
+
<pentry minsize="1" maxsize="4">
|
|
39
|
+
<register name="a1"/>
|
|
40
|
+
</pentry>
|
|
41
|
+
<pentry minsize="1" maxsize="4">
|
|
42
|
+
<register name="a2"/>
|
|
43
|
+
</pentry>
|
|
44
|
+
<pentry minsize="1" maxsize="4">
|
|
45
|
+
<register name="a3"/>
|
|
46
|
+
</pentry>
|
|
47
|
+
<pentry minsize="1" maxsize="500" align="4">
|
|
48
|
+
<addr offset="16" space="stack"/>
|
|
49
|
+
</pentry>
|
|
50
|
+
</input>
|
|
51
|
+
<output>
|
|
52
|
+
<pentry minsize="1" maxsize="8" metatype="float">
|
|
53
|
+
<register name="f0_1"/>
|
|
54
|
+
</pentry>
|
|
55
|
+
<pentry minsize="1" maxsize="4">
|
|
56
|
+
<register name="v0"/>
|
|
57
|
+
</pentry>
|
|
58
|
+
<pentry minsize="5" maxsize="8">
|
|
59
|
+
<addr space="join" piece1="v0" piece2="v1"/>
|
|
60
|
+
</pentry>
|
|
61
|
+
</output>
|
|
62
|
+
<unaffected>
|
|
63
|
+
<register name="s0"/>
|
|
64
|
+
<register name="s1"/>
|
|
65
|
+
<register name="s2"/>
|
|
66
|
+
<register name="s3"/>
|
|
67
|
+
<register name="s4"/>
|
|
68
|
+
<register name="s5"/>
|
|
69
|
+
<register name="s6"/>
|
|
70
|
+
<register name="s7"/>
|
|
71
|
+
<register name="s8"/>
|
|
72
|
+
<register name="sp"/>
|
|
73
|
+
<register name="gp"/>
|
|
74
|
+
<register name="f20"/>
|
|
75
|
+
<register name="f22"/>
|
|
76
|
+
<register name="f24"/>
|
|
77
|
+
<register name="f26"/>
|
|
78
|
+
<register name="f28"/>
|
|
79
|
+
<register name="f30"/>
|
|
80
|
+
</unaffected>
|
|
81
|
+
<localrange>
|
|
82
|
+
<range space="stack" first="0xfff0bdc0" last="0xffffffff"/>
|
|
83
|
+
<range space="stack" first="0" last="15"/> <!-- This is backup storage space for register params, but we treat as locals -->
|
|
84
|
+
</localrange>
|
|
85
|
+
</prototype>
|
|
86
|
+
</default_proto>
|
|
87
|
+
<prototype name="processEntry" extrapop="0" stackshift="0">
|
|
88
|
+
<input pointermax="4">
|
|
89
|
+
<pentry minsize="1" maxsize="4">
|
|
90
|
+
<register name="v0"/>
|
|
91
|
+
</pentry>
|
|
92
|
+
<pentry minsize="1" maxsize="500" align="4">
|
|
93
|
+
<addr offset="0" space="stack"/>
|
|
94
|
+
</pentry>
|
|
95
|
+
</input>
|
|
96
|
+
<output killedbycall="true">
|
|
97
|
+
<pentry minsize="1" maxsize="4">
|
|
98
|
+
<register name="v0"/>
|
|
99
|
+
</pentry>
|
|
100
|
+
</output>
|
|
101
|
+
<unaffected>
|
|
102
|
+
<register name="sp"/>
|
|
103
|
+
</unaffected>
|
|
104
|
+
</prototype>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
</compiler_spec>
|