@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -263,13 +263,16 @@ REL: target is rel [ target = inst_next + rel; ] { export *:2 target; }
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
# BRK skips its padding byte: the return address is two bytes on.
|
|
266
|
+
# The vector at $FFFE is in ROM unless the program banks the KERNAL out, and
|
|
267
|
+
# the analysis cannot know the banking. Thus BRK ends the flow, and Ghidra
|
|
268
|
+
# does not follow the RAM bytes at $FFFE of a flat image (R2-07).
|
|
266
269
|
:BRK is op=0x00 {
|
|
267
270
|
*:2 (SP - 1) = inst_next + 1;
|
|
268
271
|
SP = SP - 2;
|
|
269
272
|
pushP(1);
|
|
270
273
|
I = 1;
|
|
271
274
|
local vector:2 = 0xfffe;
|
|
272
|
-
|
|
275
|
+
return [*:2 vector];
|
|
273
276
|
}
|
|
274
277
|
|
|
275
278
|
:CLC is op=0x18 { C = 0; }
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Writes the structural result for the loaded image: functions with the
|
|
4
4
|
// source of their names, code and defined-data regions, memory references
|
|
5
|
-
// from instructions,
|
|
5
|
+
// from instructions, every instruction with its bytes, mnemonic, operand
|
|
6
|
+
// text and flow targets (D44), and the requested decompilations within their bounds.
|
|
6
7
|
//@category c64-re-tools
|
|
7
8
|
|
|
8
9
|
import java.io.FileReader;
|
|
@@ -53,6 +54,7 @@ public class C64Export extends GhidraScript {
|
|
|
53
54
|
out.add("functions", functions(coverage));
|
|
54
55
|
out.add("regions", regions(coverage));
|
|
55
56
|
out.add("references", references(coverage));
|
|
57
|
+
out.add("instructions", instructions(coverage));
|
|
56
58
|
JsonArray notDecompiled = new JsonArray();
|
|
57
59
|
out.add("decompilations", decompile(request, notDecompiled));
|
|
58
60
|
out.add("notDecompiled", notDecompiled);
|
|
@@ -156,6 +158,36 @@ public class C64Export extends GhidraScript {
|
|
|
156
158
|
return references;
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
/** Every instruction in the coverage, in address order, as Ghidra decoded it with the 6510 language. */
|
|
162
|
+
private JsonArray instructions(AddressSet coverage) throws Exception {
|
|
163
|
+
JsonArray instructions = new JsonArray();
|
|
164
|
+
for (Instruction instruction : currentProgram.getListing().getInstructions(coverage, true)) {
|
|
165
|
+
JsonObject item = new JsonObject();
|
|
166
|
+
item.addProperty("address", instruction.getMinAddress().getOffset());
|
|
167
|
+
StringBuilder bytes = new StringBuilder();
|
|
168
|
+
for (byte value : instruction.getBytes()) {
|
|
169
|
+
bytes.append(String.format("%02x", value & 0xff));
|
|
170
|
+
}
|
|
171
|
+
item.addProperty("bytes", bytes.toString());
|
|
172
|
+
item.addProperty("mnemonic", instruction.getMnemonicString().toLowerCase());
|
|
173
|
+
StringBuilder operands = new StringBuilder();
|
|
174
|
+
for (int index = 0; index < instruction.getNumOperands(); index++) {
|
|
175
|
+
if (index > 0) {
|
|
176
|
+
operands.append(",");
|
|
177
|
+
}
|
|
178
|
+
operands.append(instruction.getDefaultOperandRepresentation(index));
|
|
179
|
+
}
|
|
180
|
+
item.addProperty("operands", operands.toString());
|
|
181
|
+
JsonArray flows = new JsonArray();
|
|
182
|
+
for (Address flow : instruction.getFlows()) {
|
|
183
|
+
flows.add(flow.getOffset());
|
|
184
|
+
}
|
|
185
|
+
item.add("flows", flows);
|
|
186
|
+
instructions.add(item);
|
|
187
|
+
}
|
|
188
|
+
return instructions;
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
private static void add(JsonArray references, Set<String> seen, long from, long to, String type) {
|
|
160
192
|
if (!seen.add(from + ":" + to + ":" + type)) {
|
|
161
193
|
return;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Fills the rest of the 64 KiB address space with uninitialized memory, so
|
|
4
4
|
// references to I/O and other RAM resolve, then applies the knowledge seeds:
|
|
5
5
|
// labels (user-defined names), data ranges (defined bytes that analysis does
|
|
6
|
-
// not disassemble)
|
|
6
|
+
// not disassemble), entry points (disassembled functions) and code starts
|
|
7
|
+
// (disassembled code without a function: a branch target is no routine).
|
|
7
8
|
//@category c64-re-tools
|
|
8
9
|
|
|
9
10
|
import java.io.FileReader;
|
|
@@ -56,5 +57,10 @@ public class C64Prepare extends GhidraScript {
|
|
|
56
57
|
createFunction(entry, null);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
if (seeds.has("codeStarts")) {
|
|
61
|
+
for (JsonElement element : seeds.getAsJsonArray("codeStarts")) {
|
|
62
|
+
disassemble(toAddr(element.getAsLong()));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
}
|
package/src/native/status.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
9
9
|
import { WireFailure } from "../protocol/messages.ts";
|
|
10
10
|
import { type ToolStatus } from "../protocol/tools.ts";
|
|
11
11
|
import { ACME, DXA, findTool } from "./discover.ts";
|
|
12
|
-
import { findGhidra, ghidraVersion } from "./ghidra/index.ts";
|
|
12
|
+
import { findGhidra, ghidraVersion, NO_USABLE_JDK, noJdk } from "./ghidra/index.ts";
|
|
13
13
|
import { runTool, type ToolContext } from "./run.ts";
|
|
14
14
|
import { Workspace } from "./staging.ts";
|
|
15
15
|
|
|
@@ -35,10 +35,21 @@ export function versionLine(output: string, pattern: RegExp): string | undefined
|
|
|
35
35
|
.find((line) => pattern.test(line));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function run(
|
|
38
|
+
async function run(
|
|
39
|
+
name: string,
|
|
40
|
+
path: string,
|
|
41
|
+
args: string[],
|
|
42
|
+
version: (output: string) => string | undefined,
|
|
43
|
+
context: ToolContext,
|
|
44
|
+
/** A known cause in the output, said in place of the exit status. */
|
|
45
|
+
cause?: (output: string) => string | undefined,
|
|
46
|
+
): Promise<ToolStatus> {
|
|
39
47
|
const result = await runTool({ argv: [path, ...args], cwd: tmpdir(), supervisor: context.supervisor, signal: context.signal, timeoutMs: PROBE_TIMEOUT_MS, env: context.env });
|
|
40
|
-
const
|
|
48
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
49
|
+
const found = version(output);
|
|
41
50
|
if (found !== undefined) return { name, found: true, path, version: found, runs: true };
|
|
51
|
+
const known = cause?.(output);
|
|
52
|
+
if (known !== undefined) return { name, found: true, path, runs: false, problem: known };
|
|
42
53
|
const problem = result.timedOut ? "It did not answer in time." : result.spawnError !== undefined ? `It did not start (${result.spawnError}).` : `It did not run correctly (exit ${result.code ?? result.signal}).`;
|
|
43
54
|
return { name, found: true, path, runs: false, problem };
|
|
44
55
|
}
|
|
@@ -79,6 +90,7 @@ export async function probeGhidra(context: ToolContext): Promise<ToolStatus> {
|
|
|
79
90
|
[],
|
|
80
91
|
(output) => (/Headless Analyzer Usage/.test(output) ? ghidraVersion(ghidra) : undefined),
|
|
81
92
|
{ ...context, env: { ...env, XDG_CONFIG_HOME: settings, XDG_CACHE_HOME: settings } },
|
|
93
|
+
(output) => (NO_USABLE_JDK.test(output) ? noJdk(ghidra).message : undefined),
|
|
82
94
|
);
|
|
83
95
|
return { ...status, path: ghidra.root };
|
|
84
96
|
} finally {
|
package/src/native/types.ts
CHANGED
|
@@ -39,19 +39,32 @@ export interface AcmeResult {
|
|
|
39
39
|
export const IMAGE_KINDS = ["prg", "flat64k"] as const;
|
|
40
40
|
export type ImageKind = (typeof IMAGE_KINDS)[number];
|
|
41
41
|
|
|
42
|
-
export const GHIDRA_LIMITS = { entryPoints: 1024, dataRanges: 1024, labels: 4096, decompile: 32, decompiledChars: 16_000, decompiledTotalChars: 128_000 } as const;
|
|
42
|
+
export const GHIDRA_LIMITS = { entryPoints: 1024, codeStarts: 8192, dataRanges: 1024, labels: 4096, decompile: 32, decompiledChars: 16_000, decompiledTotalChars: 128_000 } as const;
|
|
43
43
|
|
|
44
44
|
/** The Ghidra analysis. Its image is given next to these fields: a PRG, or 64 KiB from $0000. */
|
|
45
45
|
export interface GhidraParams {
|
|
46
46
|
imageKind: ImageKind;
|
|
47
47
|
/** Seeds from current knowledge. */
|
|
48
48
|
entryPoints: number[];
|
|
49
|
+
/** Executed instruction starts that Ghidra disassembles without making a function (D74): a branch target is no routine. */
|
|
50
|
+
codeStarts?: number[];
|
|
49
51
|
dataRanges: Array<{ start: number; end: number }>;
|
|
50
52
|
labels: Array<{ address: number; name: string }>;
|
|
51
53
|
/** Routine entries to decompile for immediate reasoning; the text is never stored. */
|
|
52
54
|
decompile: number[];
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
export interface GhidraInstruction {
|
|
58
|
+
address: number;
|
|
59
|
+
/** Lowercase hex, 1 to 3 bytes. */
|
|
60
|
+
bytes: string;
|
|
61
|
+
mnemonic: string;
|
|
62
|
+
/** Ghidra's text of the operands, empty for an implied operand. */
|
|
63
|
+
operands: string;
|
|
64
|
+
/** Where the flow goes besides the next instruction: a branch, jump or call target. */
|
|
65
|
+
flows: number[];
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
export interface GhidraResult {
|
|
56
69
|
coverage: Array<{ start: number; end: number }>;
|
|
57
70
|
functions: Array<{ entry: number; name: string; nameSource: "seed" | "generated" | "native" }>;
|
|
@@ -60,6 +73,8 @@ export interface GhidraResult {
|
|
|
60
73
|
decompilations: Array<{ entry: number; text: string; truncated: boolean }>;
|
|
61
74
|
/** Requested routines without decompiled code, with the decompiler's reason. */
|
|
62
75
|
notDecompiled?: Array<{ entry: number; reason: string }>;
|
|
76
|
+
/** Every instruction in the coverage, as Ghidra decoded it (D44). */
|
|
77
|
+
instructions: GhidraInstruction[];
|
|
63
78
|
/** Which categories are complete inside the coverage; private to the importer. */
|
|
64
79
|
completeness: { functions: boolean; regions: boolean; references: boolean };
|
|
65
80
|
/** The version of the Ghidra that made the result, for example "Ghidra 12.1.3". */
|
|
@@ -67,9 +82,10 @@ export interface GhidraResult {
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
/** Refuses a DXA or Ghidra request with more seeds or decompilations than the request bounds allow, by name. */
|
|
70
|
-
export function checkRequestBounds(params: DxaParams & { decompile?: number[] }): void {
|
|
85
|
+
export function checkRequestBounds(params: DxaParams & { decompile?: number[]; codeStarts?: number[] }): void {
|
|
71
86
|
const bounds = [
|
|
72
87
|
["entry points", params.entryPoints.length, GHIDRA_LIMITS.entryPoints],
|
|
88
|
+
["code starts", params.codeStarts?.length ?? 0, GHIDRA_LIMITS.codeStarts],
|
|
73
89
|
["data ranges", params.dataRanges.length, GHIDRA_LIMITS.dataRanges],
|
|
74
90
|
["labels", params.labels.length, GHIDRA_LIMITS.labels],
|
|
75
91
|
["routines to decompile", params.decompile?.length ?? 0, GHIDRA_LIMITS.decompile],
|
|
@@ -80,7 +96,7 @@ export function checkRequestBounds(params: DxaParams & { decompile?: number[] })
|
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
/** The DXA analysis: the same image and seeds as Ghidra, without decompilation. */
|
|
83
|
-
export type DxaParams = Omit<GhidraParams, "decompile">;
|
|
99
|
+
export type DxaParams = Omit<GhidraParams, "decompile" | "codeStarts">;
|
|
84
100
|
|
|
85
101
|
export interface DxaResult {
|
|
86
102
|
coverage: Array<{ start: number; end: number }>;
|
|
@@ -115,7 +131,7 @@ const isList = (value: unknown): value is unknown[] => Array.isArray(value);
|
|
|
115
131
|
*/
|
|
116
132
|
export function checkGhidraResult(value: unknown): GhidraResult {
|
|
117
133
|
if (!isObject(value)) fail("not an object");
|
|
118
|
-
const { coverage, functions, regions, references, decompilations, notDecompiled, completeness, toolVersion } = value;
|
|
134
|
+
const { coverage, functions, regions, references, decompilations, notDecompiled, completeness, toolVersion, instructions } = value;
|
|
119
135
|
if (!isList(coverage) || coverage.length === 0) fail("no coverage");
|
|
120
136
|
const ranges: Array<{ start: number; end: number }> = [];
|
|
121
137
|
for (const range of coverage) {
|
|
@@ -138,6 +154,22 @@ export function checkGhidraResult(value: unknown): GhidraResult {
|
|
|
138
154
|
if (!ranges.some((range) => range.start <= start && end <= range.end)) fail("a region lies outside the coverage");
|
|
139
155
|
previousEnd = end;
|
|
140
156
|
}
|
|
157
|
+
if (!isList(instructions)) fail("no instructions");
|
|
158
|
+
for (const instruction of instructions) {
|
|
159
|
+
if (
|
|
160
|
+
!isObject(instruction) ||
|
|
161
|
+
!isInteger(instruction.address, 0, 0xffff) ||
|
|
162
|
+
typeof instruction.bytes !== "string" ||
|
|
163
|
+
!/^(?:[0-9a-f]{2}){1,3}$/.test(instruction.bytes) ||
|
|
164
|
+
typeof instruction.mnemonic !== "string" ||
|
|
165
|
+
typeof instruction.operands !== "string" ||
|
|
166
|
+
!isList(instruction.flows) ||
|
|
167
|
+
!instruction.flows.every((flow) => isInteger(flow, 0, 0xffff))
|
|
168
|
+
) {
|
|
169
|
+
fail("bad instruction");
|
|
170
|
+
}
|
|
171
|
+
if (!covered(instruction.address)) fail("an instruction lies outside the coverage");
|
|
172
|
+
}
|
|
141
173
|
if (!isList(references)) fail("no references");
|
|
142
174
|
for (const reference of references) {
|
|
143
175
|
if (!isObject(reference) || !isInteger(reference.from, 0, 0xffff) || !isInteger(reference.to, 0, 0xffff) || !isOneOf(REFERENCE_KINDS, reference.type)) fail("bad reference");
|
|
@@ -218,11 +250,14 @@ export function imageRange(kind: ImageKind, image: Uint8Array): { start: number;
|
|
|
218
250
|
* it, and the data ranges cut to it. A data range that holds an entry point
|
|
219
251
|
* is left out, so that the entry point is disassembled.
|
|
220
252
|
*/
|
|
221
|
-
export function seedsInside(params: Pick<
|
|
222
|
-
const
|
|
253
|
+
export function seedsInside(params: Pick<GhidraParams, "entryPoints" | "dataRanges" | "codeStarts">, start: number, end: number): { entryPoints: number[]; codeStarts: number[]; dataRanges: GhidraParams["dataRanges"] } {
|
|
254
|
+
const inside = (address: number) => address >= start && address <= end;
|
|
255
|
+
const entryPoints = params.entryPoints.filter(inside);
|
|
256
|
+
const codeStarts = (params.codeStarts ?? []).filter(inside);
|
|
257
|
+
const code = [...entryPoints, ...codeStarts];
|
|
223
258
|
const dataRanges = params.dataRanges
|
|
224
259
|
.filter((range) => range.end >= start && range.start <= end)
|
|
225
260
|
.map((range) => ({ start: Math.max(range.start, start), end: Math.min(range.end, end) }))
|
|
226
|
-
.filter((range) => !
|
|
227
|
-
return { entryPoints, dataRanges };
|
|
261
|
+
.filter((range) => !code.some((entry) => entry >= range.start && entry <= range.end));
|
|
262
|
+
return { entryPoints, codeStarts, dataRanges };
|
|
228
263
|
}
|
package/src/opcodes.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The ACME spelling of each 6510 opcode (D48, D78): the mnemonic and the addressing
|
|
2
|
+
// mode, undocumented opcodes included. Ghidra gives the instruction
|
|
3
|
+
// boundaries; this table gives the text. Where several opcodes share one
|
|
4
|
+
// spelling, ACME writes only one of them: the generator finds those with a
|
|
5
|
+
// probe and writes the others as bytes.
|
|
6
|
+
|
|
7
|
+
export type Mode = "imp" | "imm" | "zp" | "zpx" | "zpy" | "abs" | "abx" | "aby" | "ind" | "izx" | "izy" | "rel";
|
|
8
|
+
|
|
9
|
+
export interface Opcode {
|
|
10
|
+
mnemonic: string;
|
|
11
|
+
mode: Mode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ROWS = `
|
|
15
|
+
00 brk imp|01 ora izx|02 jam imp|03 slo izx|04 nop zp|05 ora zp|06 asl zp|07 slo zp|08 php imp|09 ora imm|0a asl imp|0b anc imm|0c nop abs|0d ora abs|0e asl abs|0f slo abs
|
|
16
|
+
10 bpl rel|11 ora izy|12 jam imp|13 slo izy|14 nop zpx|15 ora zpx|16 asl zpx|17 slo zpx|18 clc imp|19 ora aby|1a nop imp|1b slo aby|1c nop abx|1d ora abx|1e asl abx|1f slo abx
|
|
17
|
+
20 jsr abs|21 and izx|22 jam imp|23 rla izx|24 bit zp|25 and zp|26 rol zp|27 rla zp|28 plp imp|29 and imm|2a rol imp|2b anc imm|2c bit abs|2d and abs|2e rol abs|2f rla abs
|
|
18
|
+
30 bmi rel|31 and izy|32 jam imp|33 rla izy|34 nop zpx|35 and zpx|36 rol zpx|37 rla zpx|38 sec imp|39 and aby|3a nop imp|3b rla aby|3c nop abx|3d and abx|3e rol abx|3f rla abx
|
|
19
|
+
40 rti imp|41 eor izx|42 jam imp|43 sre izx|44 nop zp|45 eor zp|46 lsr zp|47 sre zp|48 pha imp|49 eor imm|4a lsr imp|4b asr imm|4c jmp abs|4d eor abs|4e lsr abs|4f sre abs
|
|
20
|
+
50 bvc rel|51 eor izy|52 jam imp|53 sre izy|54 nop zpx|55 eor zpx|56 lsr zpx|57 sre zpx|58 cli imp|59 eor aby|5a nop imp|5b sre aby|5c nop abx|5d eor abx|5e lsr abx|5f sre abx
|
|
21
|
+
60 rts imp|61 adc izx|62 jam imp|63 rra izx|64 nop zp|65 adc zp|66 ror zp|67 rra zp|68 pla imp|69 adc imm|6a ror imp|6b arr imm|6c jmp ind|6d adc abs|6e ror abs|6f rra abs
|
|
22
|
+
70 bvs rel|71 adc izy|72 jam imp|73 rra izy|74 nop zpx|75 adc zpx|76 ror zpx|77 rra zpx|78 sei imp|79 adc aby|7a nop imp|7b rra aby|7c nop abx|7d adc abx|7e ror abx|7f rra abx
|
|
23
|
+
80 nop imm|81 sta izx|82 nop imm|83 sax izx|84 sty zp|85 sta zp|86 stx zp|87 sax zp|88 dey imp|89 nop imm|8a txa imp|8b ane imm|8c sty abs|8d sta abs|8e stx abs|8f sax abs
|
|
24
|
+
90 bcc rel|91 sta izy|92 jam imp|93 sha izy|94 sty zpx|95 sta zpx|96 stx zpy|97 sax zpy|98 tya imp|99 sta aby|9a txs imp|9b tas aby|9c shy abx|9d sta abx|9e shx aby|9f sha aby
|
|
25
|
+
a0 ldy imm|a1 lda izx|a2 ldx imm|a3 lax izx|a4 ldy zp|a5 lda zp|a6 ldx zp|a7 lax zp|a8 tay imp|a9 lda imm|aa tax imp|ab lxa imm|ac ldy abs|ad lda abs|ae ldx abs|af lax abs
|
|
26
|
+
b0 bcs rel|b1 lda izy|b2 jam imp|b3 lax izy|b4 ldy zpx|b5 lda zpx|b6 ldx zpy|b7 lax zpy|b8 clv imp|b9 lda aby|ba tsx imp|bb las aby|bc ldy abx|bd lda abx|be ldx aby|bf lax aby
|
|
27
|
+
c0 cpy imm|c1 cmp izx|c2 nop imm|c3 dcp izx|c4 cpy zp|c5 cmp zp|c6 dec zp|c7 dcp zp|c8 iny imp|c9 cmp imm|ca dex imp|cb sbx imm|cc cpy abs|cd cmp abs|ce dec abs|cf dcp abs
|
|
28
|
+
d0 bne rel|d1 cmp izy|d2 jam imp|d3 dcp izy|d4 nop zpx|d5 cmp zpx|d6 dec zpx|d7 dcp zpx|d8 cld imp|d9 cmp aby|da nop imp|db dcp aby|dc nop abx|dd cmp abx|de dec abx|df dcp abx
|
|
29
|
+
e0 cpx imm|e1 sbc izx|e2 nop imm|e3 isc izx|e4 cpx zp|e5 sbc zp|e6 inc zp|e7 isc zp|e8 inx imp|e9 sbc imm|ea nop imp|eb sbc imm|ec cpx abs|ed sbc abs|ee inc abs|ef isc abs
|
|
30
|
+
f0 beq rel|f1 sbc izy|f2 jam imp|f3 isc izy|f4 nop zpx|f5 sbc zpx|f6 inc zpx|f7 isc zpx|f8 sed imp|f9 sbc aby|fa nop imp|fb isc aby|fc nop abx|fd sbc abx|fe inc abx|ff isc abx`;
|
|
31
|
+
|
|
32
|
+
export const OPCODES: readonly Opcode[] = (() => {
|
|
33
|
+
const table: Opcode[] = [];
|
|
34
|
+
for (const cell of ROWS.trim().split(/[|\n]/)) {
|
|
35
|
+
const [code, mnemonic, mode] = cell.trim().split(" ");
|
|
36
|
+
table[Number.parseInt(code!, 16)] = { mnemonic: mnemonic!, mode: mode as Mode };
|
|
37
|
+
}
|
|
38
|
+
return table;
|
|
39
|
+
})();
|
|
40
|
+
|
|
41
|
+
export const MODE_LENGTH: Record<Mode, 1 | 2 | 3> = { imp: 1, imm: 2, zp: 2, zpx: 2, zpy: 2, izx: 2, izy: 2, rel: 2, abs: 3, abx: 3, aby: 3, ind: 3 };
|
|
42
|
+
|
|
43
|
+
/** Every mnemonic in the table: a symbol with one of these names would read as an instruction. */
|
|
44
|
+
export const MNEMONICS: ReadonlySet<string> = new Set(OPCODES.map((opcode) => opcode.mnemonic));
|
|
45
|
+
|
|
46
|
+
const ZERO_PAGE_OF: Partial<Record<Mode, Mode>> = { abs: "zp", abx: "zpx", aby: "zpy" };
|
|
47
|
+
|
|
48
|
+
/** True when the mnemonic also has the zero-page form of an absolute mode: ACME would pick it for a small operand. */
|
|
49
|
+
export function hasZeroPageForm(mnemonic: string, mode: Mode): boolean {
|
|
50
|
+
const zeroPage = ZERO_PAGE_OF[mode];
|
|
51
|
+
return zeroPage !== undefined && OPCODES.some((opcode) => opcode.mnemonic === mnemonic && opcode.mode === zeroPage);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True for a mode whose operand is an address (or a branch target), so a name can stand for it. */
|
|
55
|
+
export function takesAddress(mode: Mode): boolean {
|
|
56
|
+
return mode !== "imp" && mode !== "imm";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** How an instruction uses the address of its operand, in the order a summary lists them. */
|
|
60
|
+
export const USE_KINDS = ["read", "written", "modified", "called", "jumped to", "jumped through", "branched to", "pointer", "address", "table", "macro argument"] as const;
|
|
61
|
+
export type UseKind = (typeof USE_KINDS)[number];
|
|
62
|
+
|
|
63
|
+
const WRITES = new Set(["sta", "stx", "sty", "sax", "sha", "shx", "shy", "tas"]);
|
|
64
|
+
const MODIFIES = new Set(["inc", "dec", "asl", "lsr", "rol", "ror", "slo", "rla", "sre", "rra", "dcp", "isc"]);
|
|
65
|
+
|
|
66
|
+
/** The use of the operand address of an instruction that takes an address. Through (zp),y and (zp,x) the address is a pointer. */
|
|
67
|
+
export function useKind(opcode: number): UseKind {
|
|
68
|
+
const { mnemonic, mode } = OPCODES[opcode]!;
|
|
69
|
+
if (mode === "izx" || mode === "izy") return "pointer";
|
|
70
|
+
if (mode === "rel") return "branched to";
|
|
71
|
+
if (mnemonic === "jsr") return "called";
|
|
72
|
+
if (mnemonic === "jmp") return mode === "ind" ? "jumped through" : "jumped to";
|
|
73
|
+
if (WRITES.has(mnemonic)) return "written";
|
|
74
|
+
if (MODIFIES.has(mnemonic)) return "modified";
|
|
75
|
+
return "read";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
79
|
+
const hex4 = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
80
|
+
|
|
81
|
+
/** The text of the instruction in `bytes` at `pc`, for example "sta $d011" or "bne $c002". */
|
|
82
|
+
export function instructionText(bytes: readonly number[], pc: number): string {
|
|
83
|
+
const { mnemonic, mode } = OPCODES[bytes[0]!]!;
|
|
84
|
+
const low = bytes[1] ?? 0;
|
|
85
|
+
const word = low | ((bytes[2] ?? 0) << 8);
|
|
86
|
+
const operand: Record<Mode, string> = {
|
|
87
|
+
imp: "",
|
|
88
|
+
imm: `#${hex2(low)}`,
|
|
89
|
+
zp: hex2(low),
|
|
90
|
+
zpx: `${hex2(low)},x`,
|
|
91
|
+
zpy: `${hex2(low)},y`,
|
|
92
|
+
abs: hex4(word),
|
|
93
|
+
abx: `${hex4(word)},x`,
|
|
94
|
+
aby: `${hex4(word)},y`,
|
|
95
|
+
ind: `(${hex4(word)})`,
|
|
96
|
+
izx: `(${hex2(low)},x)`,
|
|
97
|
+
izy: `(${hex2(low)}),y`,
|
|
98
|
+
rel: hex4((pc + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff),
|
|
99
|
+
};
|
|
100
|
+
return operand[mode] === "" ? mnemonic : `${mnemonic} ${operand[mode]}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The address that a read or write of the instruction in `bytes` uses, with X
|
|
105
|
+
* and Y as they were before it; `byteAt` reads a zero-page pointer. Undefined
|
|
106
|
+
* for a mode that uses no memory operand.
|
|
107
|
+
*/
|
|
108
|
+
export function operandAddress(bytes: readonly number[], x: number, y: number, byteAt: (address: number) => number): number | undefined {
|
|
109
|
+
const { mode } = OPCODES[bytes[0]!]!;
|
|
110
|
+
const low = bytes[1] ?? 0;
|
|
111
|
+
const word = low | ((bytes[2] ?? 0) << 8);
|
|
112
|
+
const pointer = (at: number) => byteAt(at & 0xff) | (byteAt((at + 1) & 0xff) << 8);
|
|
113
|
+
switch (mode) {
|
|
114
|
+
case "zp":
|
|
115
|
+
return low;
|
|
116
|
+
case "zpx":
|
|
117
|
+
return (low + x) & 0xff;
|
|
118
|
+
case "zpy":
|
|
119
|
+
return (low + y) & 0xff;
|
|
120
|
+
case "abs":
|
|
121
|
+
return word;
|
|
122
|
+
case "abx":
|
|
123
|
+
return (word + x) & 0xffff;
|
|
124
|
+
case "aby":
|
|
125
|
+
return (word + y) & 0xffff;
|
|
126
|
+
case "izx":
|
|
127
|
+
return pointer(low + x);
|
|
128
|
+
case "izy":
|
|
129
|
+
return (pointer(low) + y) & 0xffff;
|
|
130
|
+
default:
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/package.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The name, version and npm channel of this copy of c64-re-tools, for the npx
|
|
2
|
+
// commands that refusals and help texts give. The package.json above src/ is
|
|
3
|
+
// the package's own one; in an installed skill it is the skill's one, which
|
|
4
|
+
// has the same name and version (distribution/plugin.ts).
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The npm dist-tag that a version is published under: a prerelease
|
|
11
|
+
* (2.0.0-rc.1) goes to "next", a release to "latest".
|
|
12
|
+
*/
|
|
13
|
+
export function channelOf(version: string): "latest" | "next" {
|
|
14
|
+
return version.includes("-") ? "next" : "latest";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The name, version and dist-tag of this npm package, from its package.json. */
|
|
18
|
+
export const PACKAGE = (() => {
|
|
19
|
+
const metadata = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { name: string; version: string };
|
|
20
|
+
return { name: metadata.name, version: metadata.version, tag: channelOf(metadata.version) };
|
|
21
|
+
})();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// C64 platform reference data and decoders
|
|
1
|
+
// C64 platform reference data and decoders: the c64-memory-map script, and the
|
|
2
|
+
// platform names that c64-source uses (D52).
|
|
2
3
|
// Names follow "Mapping the Commodore 64". Only platform facts live here:
|
|
3
4
|
// an address without one belongs to the application.
|
|
4
5
|
|
|
5
|
-
import { d018Offsets, vicBankBase } from "
|
|
6
|
+
import { d018Offsets, vicBankBase } from "./c64.ts";
|
|
6
7
|
|
|
7
8
|
export interface Entry {
|
|
8
9
|
start: number;
|
|
@@ -322,10 +323,33 @@ const AREAS: Array<{ start: number; end: number; area: string; kind: Lookup["kin
|
|
|
322
323
|
|
|
323
324
|
const hex = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
324
325
|
|
|
326
|
+
export interface Area {
|
|
327
|
+
name: string;
|
|
328
|
+
start: number;
|
|
329
|
+
end: number;
|
|
330
|
+
kind: Lookup["kind"];
|
|
331
|
+
/** What a memory area holds. A chip register block has none: its name says it. */
|
|
332
|
+
meaning?: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The memory area or the I/O register block of an address. */
|
|
336
|
+
export function areaOf(address: number): Area {
|
|
337
|
+
const block = IO.find((candidate) => address >= candidate.start && address <= candidate.end);
|
|
338
|
+
if (block !== undefined) return { name: block.area, start: block.start, end: block.end, kind: "platform" };
|
|
339
|
+
const area = AREAS.find((candidate) => address >= candidate.start && address <= candidate.end)!;
|
|
340
|
+
return { name: area.area, start: area.start, end: area.end, kind: area.kind, meaning: area.meaning };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The area of an address, and the register that an I/O mirror repeats: c64-source groups and describes its constants with them. */
|
|
344
|
+
export function describeAddress(address: number): { area: Area; mirrorOf?: number } {
|
|
345
|
+
const { mirrorOf } = lookup(address);
|
|
346
|
+
return { area: areaOf(address), ...(mirrorOf === undefined ? {} : { mirrorOf }) };
|
|
347
|
+
}
|
|
348
|
+
|
|
325
349
|
/** The platform meaning of one address, or its application area. */
|
|
326
350
|
export function lookup(address: number): Lookup {
|
|
327
|
-
|
|
328
|
-
|
|
351
|
+
const block = IO.find((candidate) => address >= candidate.start && address <= candidate.end);
|
|
352
|
+
if (block !== undefined) {
|
|
329
353
|
const base = block.start + ((address - block.start) % block.period);
|
|
330
354
|
const register = block.registers.find((entry) => entry.start === base);
|
|
331
355
|
const mirror = base !== address ? { mirrorOf: base } : {};
|
|
@@ -333,11 +357,67 @@ export function lookup(address: number): Lookup {
|
|
|
333
357
|
return { address, kind: "platform", area: block.area, name: register.name, meaning: register.meaning, ...mirror };
|
|
334
358
|
}
|
|
335
359
|
const entry = [...SYSTEM, ...ROM].find((candidate) => address >= candidate.start && address <= candidate.end);
|
|
336
|
-
const area =
|
|
360
|
+
const area = areaOf(address);
|
|
337
361
|
if (entry !== undefined) {
|
|
338
|
-
return { address, kind: "platform", area: area.
|
|
362
|
+
return { address, kind: "platform", area: area.name, name: entry.name, meaning: entry.meaning, ...(entry.start !== address ? { entryStart: entry.start } : {}) };
|
|
339
363
|
}
|
|
340
|
-
return { address, kind: area.kind, area: area.
|
|
364
|
+
return { address, kind: area.kind, area: area.name, meaning: area.meaning! };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The platform name of each named address (D52): system variables, ROM
|
|
369
|
+
* entries and I/O registers, not mirrors, in address order. Only names that
|
|
370
|
+
* an assembler can use as a symbol, and each name once, at its lowest address.
|
|
371
|
+
* `end` is the last address of the entry: a vector has two bytes.
|
|
372
|
+
*/
|
|
373
|
+
export function platformNames(): Array<{ address: number; end: number; name: string; meaning: string }> {
|
|
374
|
+
const io = IO.flatMap((block) => block.registers);
|
|
375
|
+
const seen = new Set<string>();
|
|
376
|
+
return [...SYSTEM, ...ROM, ...io]
|
|
377
|
+
.map((entry) => ({ address: entry.start, end: entry.end, name: entry.name, meaning: entry.meaning }))
|
|
378
|
+
.filter((entry) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.name))
|
|
379
|
+
.sort((a, b) => a.address - b.address)
|
|
380
|
+
.filter((entry) => !seen.has(entry.name) && seen.add(entry.name) !== undefined);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Which ROMs a program uses (D75): a ROM it does not use gives no names, and without both the system area gives none. */
|
|
384
|
+
export interface RomUse {
|
|
385
|
+
basic: boolean;
|
|
386
|
+
kernal: boolean;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The platform names of a program that uses only some ROMs (D75): the BASIC
|
|
391
|
+
* ROM entries need the BASIC ROM, the KERNAL ROM entries the KERNAL, and the
|
|
392
|
+
* system variables of $0002-$03FF (the workspace of both) one of them. The
|
|
393
|
+
* processor port and the I/O registers always stay.
|
|
394
|
+
*/
|
|
395
|
+
export function platformNamesFor(use: RomUse): ReturnType<typeof platformNames> {
|
|
396
|
+
return platformNames().filter((entry) => {
|
|
397
|
+
if (entry.address >= 0xa000 && entry.address <= 0xbfff) return use.basic;
|
|
398
|
+
if (entry.address >= 0xe000) return use.kernal;
|
|
399
|
+
if (entry.address >= 0x0002 && entry.address <= 0x03ff) return use.basic || use.kernal;
|
|
400
|
+
return true;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The ROMs that a coverage map shows in use: code of the ROM executed. A read
|
|
406
|
+
* of ROM bytes is data, for example a table or random numbers, and does not use
|
|
407
|
+
* the routines of the ROM or their workspace (Bruce Lee reads 1,571 bytes of the
|
|
408
|
+
* BASIC ROM and executes none).
|
|
409
|
+
*/
|
|
410
|
+
export function romUseOf(coverage: Uint8Array, romExecute: number): RomUse {
|
|
411
|
+
const used = (start: number, end: number) => {
|
|
412
|
+
for (let address = start; address <= end; address++) if ((coverage[address]! & romExecute) !== 0) return true;
|
|
413
|
+
return false;
|
|
414
|
+
};
|
|
415
|
+
return { basic: used(0xa000, 0xbfff), kernal: used(0xe000, 0xffff) };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The ROMs that a value of $01 switches in: HIRAM for the KERNAL, LORAM and HIRAM for BASIC. */
|
|
419
|
+
export function romUseOfPort(port: number): RomUse {
|
|
420
|
+
return { basic: (port & 0x03) === 0x03, kernal: (port & 0x02) !== 0 };
|
|
341
421
|
}
|
|
342
422
|
|
|
343
423
|
export type Decoded = Record<string, unknown>;
|