@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
|
@@ -5,6 +5,7 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
5
5
|
|
|
6
6
|
import type { NormalizedFindings } from "#src/knowledge/import.ts";
|
|
7
7
|
import { listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
8
|
+
import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
8
9
|
import type { DxaResult, GhidraParams, GhidraResult } from "#src/native/types.ts";
|
|
9
10
|
|
|
10
11
|
export type Seeds = Pick<GhidraParams, "entryPoints" | "dataRanges" | "labels">;
|
|
@@ -18,13 +19,14 @@ const semantic = (row: { origin: string }) => row.origin === "user" || row.origi
|
|
|
18
19
|
* earlier guess would force the same answer again, and a re-analysis could
|
|
19
20
|
* not retire it.
|
|
20
21
|
*/
|
|
21
|
-
export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = []): Seeds {
|
|
22
|
-
const symbols = listSymbols(db).filter(semantic);
|
|
22
|
+
export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = [], space: Space = DEFAULT_SPACE): Seeds {
|
|
23
|
+
const symbols = listSymbols(db, undefined, undefined, undefined, space).filter(semantic);
|
|
23
24
|
const entryPoints = [...new Set([...extraEntryPoints, ...symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address)])].sort((a, b) => a - b);
|
|
24
25
|
return {
|
|
25
26
|
entryPoints,
|
|
26
|
-
dataRanges: listRegions(db)
|
|
27
|
-
|
|
27
|
+
dataRanges: listRegions(db, undefined, undefined, undefined, space)
|
|
28
|
+
// A JMP table is code and dual-use bytes are both: neither is a data range (D42).
|
|
29
|
+
.filter((region) => semantic(region) && !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
|
|
28
30
|
.map((region) => ({ start: region.start, end: region.end })),
|
|
29
31
|
labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
|
|
30
32
|
};
|
|
@@ -44,11 +46,15 @@ export function ghidraFindings(result: GhidraResult): NormalizedFindings {
|
|
|
44
46
|
for (const fn of result.functions) uses.set(fn.name, (uses.get(fn.name) ?? 0) + 1);
|
|
45
47
|
const nameOf = (fn: GhidraResult["functions"][number]) =>
|
|
46
48
|
fn.nameSource !== "seed" && uses.get(fn.name)! > 1 ? `${fn.name}_${fn.entry.toString(16).padStart(4, "0")}` : fn.name;
|
|
49
|
+
// A function that branches and jumps reach, and no call, is a place in a routine: a label (D74).
|
|
50
|
+
const called = new Set(result.references.filter((reference) => reference.type === "call").map((reference) => reference.to));
|
|
51
|
+
const jumped = new Set(result.references.filter((reference) => reference.type === "jump").map((reference) => reference.to));
|
|
52
|
+
const kindOf = (fn: GhidraResult["functions"][number]) => (jumped.has(fn.entry) && !called.has(fn.entry) ? ("label" as const) : ("routine" as const));
|
|
47
53
|
return {
|
|
48
54
|
analyzer: "ghidra",
|
|
49
55
|
coverage: result.coverage,
|
|
50
56
|
authoritative: { symbols: result.completeness.functions, regions: result.completeness.regions, references: result.completeness.references },
|
|
51
|
-
symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind:
|
|
57
|
+
symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind: kindOf(fn) })),
|
|
52
58
|
regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
|
|
53
59
|
references: result.references.map((reference) => ({ from: reference.from, to: reference.to, kind: reference.type })),
|
|
54
60
|
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// 6502 idioms that hide code from a disassembler (D44): RTS dispatch tables,
|
|
2
|
+
// JMP tables and BIT skips are found by their exact instruction patterns and
|
|
3
|
+
// become heuristic findings and entry points; DOP padding and self-modified
|
|
4
|
+
// opcodes are only reported. The CBM80 header of a cartridge bank at $8000
|
|
5
|
+
// gives its two start vectors as entry points (F-29). A self-modified opcode is one that one run wrote
|
|
6
|
+
// and executed, in either order, since a coverage file holds no order (R3-04):
|
|
7
|
+
// a copier's writes in one run and their execution in another do not count (F-16). A table's length is not known statically: entries
|
|
8
|
+
// count while each target is a plausible instruction in the program.
|
|
9
|
+
|
|
10
|
+
import { COVERAGE } from "#src/c64.ts";
|
|
11
|
+
import type { NormalizedFindings } from "#src/knowledge/import.ts";
|
|
12
|
+
import type { ImageByte } from "./coverage.ts";
|
|
13
|
+
|
|
14
|
+
const MAX_TABLE = 64;
|
|
15
|
+
const MIN_JUMPS = 3;
|
|
16
|
+
const LDA_INDEXED = new Set([0xbd, 0xb9]);
|
|
17
|
+
const JAMS = new Set([0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
|
|
18
|
+
/** Undocumented NOPs with an operand byte (DOP): padding between instructions hides the real ones. */
|
|
19
|
+
const DOPS = new Set([0x04, 0x14, 0x34, 0x44, 0x54, 0x64, 0x74, 0x80, 0x82, 0x89, 0xc2, 0xd4, 0xe2, 0xf4]);
|
|
20
|
+
/** "CBM80" in PETSCII at $8004: the KERNAL then starts the cartridge through the vectors at $8000 (cold) and $8002 (warm). */
|
|
21
|
+
const CBM80 = [0xc3, 0xc2, 0xcd, 0x38, 0x30];
|
|
22
|
+
|
|
23
|
+
export interface Idioms {
|
|
24
|
+
findings: NormalizedFindings;
|
|
25
|
+
/** Entry points for the same analysis run: analyzer facts are never seeds. */
|
|
26
|
+
entryPoints: number[];
|
|
27
|
+
/** Table ranges for the same run: they are data. */
|
|
28
|
+
dataRanges: Array<{ start: number; end: number }>;
|
|
29
|
+
counts: { cartridgeHeaders: number; rtsTables: number; jumpTables: number; bitSkips: number };
|
|
30
|
+
/** Seen in the coverage, not written: the LLM decides. All of them: the result lists the first ones. */
|
|
31
|
+
candidates: { dop: number[]; selfModified: number[] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True when an instruction can start at the address: inside the program, and no JAM. */
|
|
35
|
+
function plausible(byteAt: ImageByte, address: number, range: { start: number; end: number }): boolean {
|
|
36
|
+
const opcode = byteAt(address);
|
|
37
|
+
return address >= range.start && address <= range.end && opcode !== undefined && !JAMS.has(opcode);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const word = (byteAt: ImageByte, address: number) => {
|
|
41
|
+
const low = byteAt(address);
|
|
42
|
+
const high = byteAt(address + 1);
|
|
43
|
+
return low === undefined || high === undefined ? undefined : low | (high << 8);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** `covered` is the coverage of the knowledge (D83): the joined map, and the addresses that one run wrote and executed. */
|
|
47
|
+
export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, covered?: { map: Uint8Array; selfModified: Uint8Array }): Idioms {
|
|
48
|
+
const coverage = covered?.map;
|
|
49
|
+
const regions: NormalizedFindings["regions"] = [];
|
|
50
|
+
const references: NormalizedFindings["references"] = [];
|
|
51
|
+
const entryPoints = new Set<number>();
|
|
52
|
+
const counts = { cartridgeHeaders: 0, rtsTables: 0, jumpTables: 0, bitSkips: 0 };
|
|
53
|
+
const taken = (start: number, end: number) => regions.some((region) => start <= region.end && end >= region.start);
|
|
54
|
+
const addRegion = (start: number, end: number, type: NormalizedFindings["regions"][number]["type"]) => {
|
|
55
|
+
if (start < range.start || end > range.end || taken(start, end)) return false;
|
|
56
|
+
regions.push({ start, end, type });
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (range.start <= 0x8000 && range.end >= 0x8008 && CBM80.every((byte, index) => byteAt(0x8004 + index) === byte)) {
|
|
61
|
+
const vectors = [word(byteAt, 0x8000)!, word(byteAt, 0x8002)!].filter((target) => plausible(byteAt, target, range));
|
|
62
|
+
if (addRegion(0x8000, 0x8003, "addresses") && addRegion(0x8004, 0x8008, "petscii")) {
|
|
63
|
+
counts.cartridgeHeaders++;
|
|
64
|
+
for (const target of vectors) entryPoints.add(target);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let at = range.start; at + 8 <= range.end; at++) {
|
|
69
|
+
// RTS dispatch: LDA high,X / PHA / LDA low,X / PHA / RTS.
|
|
70
|
+
if (LDA_INDEXED.has(byteAt(at)!) && byteAt(at + 3) === 0x48 && LDA_INDEXED.has(byteAt(at + 4)!) && byteAt(at + 7) === 0x48 && byteAt(at + 8) === 0x60) {
|
|
71
|
+
const high = word(byteAt, at + 1)!;
|
|
72
|
+
const low = word(byteAt, at + 5)!;
|
|
73
|
+
const interleaved = high === low + 1;
|
|
74
|
+
const step = interleaved ? 2 : 1;
|
|
75
|
+
const targets: number[] = [];
|
|
76
|
+
for (let index = 0; index < MAX_TABLE; index++) {
|
|
77
|
+
const lowByte = byteAt(low + index * step);
|
|
78
|
+
const highByte = byteAt(high + index * step);
|
|
79
|
+
if (lowByte === undefined || highByte === undefined) break;
|
|
80
|
+
const target = ((highByte << 8) | lowByte) + 1;
|
|
81
|
+
if (!plausible(byteAt, target, range)) break;
|
|
82
|
+
targets.push(target);
|
|
83
|
+
}
|
|
84
|
+
if (targets.length === 0) continue;
|
|
85
|
+
const n = targets.length;
|
|
86
|
+
const written = interleaved
|
|
87
|
+
? addRegion(low, low + 2 * n - 1, "rts-addresses")
|
|
88
|
+
: low + n === high
|
|
89
|
+
? addRegion(low, high + n - 1, "rts-addresses-split")
|
|
90
|
+
: addRegion(low, low + n - 1, "bytes") && addRegion(high, high + n - 1, "bytes");
|
|
91
|
+
if (!written) continue;
|
|
92
|
+
counts.rtsTables++;
|
|
93
|
+
for (const target of targets) {
|
|
94
|
+
references.push({ from: at + 8, to: target, kind: "jump" });
|
|
95
|
+
entryPoints.add(target);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// JMP tables: at least three JMP abs in a row.
|
|
101
|
+
for (let at = range.start; at + 3 * MIN_JUMPS - 1 <= range.end; at++) {
|
|
102
|
+
let n = 0;
|
|
103
|
+
while (at + 3 * n + 2 <= range.end && byteAt(at + 3 * n) === 0x4c) n++;
|
|
104
|
+
if (n < MIN_JUMPS || !addRegion(at, at + 3 * n - 1, "jumps")) continue;
|
|
105
|
+
counts.jumpTables++;
|
|
106
|
+
for (let index = 0; index < n; index++) {
|
|
107
|
+
entryPoints.add(at + 3 * index);
|
|
108
|
+
const target = word(byteAt, at + 3 * index + 1)!;
|
|
109
|
+
if (plausible(byteAt, target, range)) entryPoints.add(target);
|
|
110
|
+
}
|
|
111
|
+
at += 3 * n - 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// BIT skips: BIT abs ($2c) whose operand bytes are an instruction that a JMP, a JSR or the coverage enters.
|
|
115
|
+
const entered = new Set<number>();
|
|
116
|
+
for (let at = range.start; at + 2 <= range.end; at++) {
|
|
117
|
+
if (byteAt(at) === 0x4c || byteAt(at) === 0x20) entered.add(word(byteAt, at + 1)!);
|
|
118
|
+
}
|
|
119
|
+
if (coverage !== undefined) for (let address = range.start; address <= range.end; address++) if ((coverage[address]! & COVERAGE.ramExecute) !== 0) entered.add(address);
|
|
120
|
+
for (let at = range.start; at + 2 <= range.end; at++) {
|
|
121
|
+
if (byteAt(at) !== 0x2c || !entered.has(at + 1) || !plausible(byteAt, at + 1, range)) continue;
|
|
122
|
+
if (!addRegion(at + 1, at + 2, "dual-use")) continue;
|
|
123
|
+
counts.bitSkips++;
|
|
124
|
+
entryPoints.add(at);
|
|
125
|
+
entryPoints.add(at + 1);
|
|
126
|
+
for (let from = range.start; from + 2 <= range.end; from++) {
|
|
127
|
+
if ((byteAt(from) === 0x4c || byteAt(from) === 0x20) && word(byteAt, from + 1) === at + 1) references.push({ from, to: at + 1, kind: byteAt(from) === 0x20 ? "call" : "jump" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Candidates from the coverage: DOP padding that ran, and opcodes that one run wrote and executed.
|
|
132
|
+
const candidates = { dop: [] as number[], selfModified: [] as number[] };
|
|
133
|
+
if (coverage !== undefined) {
|
|
134
|
+
for (let address = range.start; address <= range.end; address++) {
|
|
135
|
+
if ((coverage[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
136
|
+
if (DOPS.has(byteAt(address)!)) candidates.dop.push(address);
|
|
137
|
+
if (covered!.selfModified[address] === 1) candidates.selfModified.push(address);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
regions.sort((a, b) => a.start - b.start);
|
|
142
|
+
const dataRanges = regions.filter((region) => region.type !== "jumps" && region.type !== "dual-use").map(({ start, end }) => ({ start, end }));
|
|
143
|
+
return {
|
|
144
|
+
findings: {
|
|
145
|
+
analyzer: "heuristic",
|
|
146
|
+
coverage: [range],
|
|
147
|
+
authoritative: { symbols: true, regions: true, references: true },
|
|
148
|
+
symbols: [],
|
|
149
|
+
regions,
|
|
150
|
+
// Each reference once: the importer keys them by from, to and kind.
|
|
151
|
+
references: [...new Map(references.map((reference) => [`${reference.from}|${reference.to}|${reference.kind}`, reference])).values()],
|
|
152
|
+
},
|
|
153
|
+
entryPoints: [...entryPoints].sort((a, b) => a - b),
|
|
154
|
+
dataRanges,
|
|
155
|
+
counts,
|
|
156
|
+
candidates,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The structured instruction list (D44): Ghidra's decoded instructions with
|
|
2
|
+
// the addressing mode, the operand in 6502 syntax, the flow target and,
|
|
3
|
+
// with a coverage file, whether the instruction ran.
|
|
4
|
+
|
|
5
|
+
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
6
|
+
import type { GhidraInstruction } from "#src/native/types.ts";
|
|
7
|
+
|
|
8
|
+
export type Mode =
|
|
9
|
+
| "implied"
|
|
10
|
+
| "accumulator"
|
|
11
|
+
| "immediate"
|
|
12
|
+
| "zeropage"
|
|
13
|
+
| "zeropage,x"
|
|
14
|
+
| "zeropage,y"
|
|
15
|
+
| "absolute"
|
|
16
|
+
| "absolute,x"
|
|
17
|
+
| "absolute,y"
|
|
18
|
+
| "(indirect)"
|
|
19
|
+
| "(indirect,x)"
|
|
20
|
+
| "(indirect),y"
|
|
21
|
+
| "relative";
|
|
22
|
+
|
|
23
|
+
const BRANCHES = new Set([0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
|
|
24
|
+
const JMP_INDIRECT = 0x6c;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The addressing mode from Ghidra's operand text and the instruction length.
|
|
28
|
+
* Ghidra prints zero page and absolute alike, and JMP (indirect) as an
|
|
29
|
+
* address, so the length and the opcode decide those.
|
|
30
|
+
*/
|
|
31
|
+
export function modeOf(opcode: number, length: number, operands: string): Mode {
|
|
32
|
+
const text = operands.toUpperCase();
|
|
33
|
+
if (text === "") return "implied";
|
|
34
|
+
if (text === "A") return "accumulator";
|
|
35
|
+
if (text.startsWith("#")) return "immediate";
|
|
36
|
+
if (text.startsWith("(") && text.endsWith(",X)")) return "(indirect,x)";
|
|
37
|
+
if (text.startsWith("(") && text.endsWith("),Y")) return "(indirect),y";
|
|
38
|
+
if (text.endsWith(",X")) return length === 2 ? "zeropage,x" : "absolute,x";
|
|
39
|
+
if (text.endsWith(",Y")) return length === 2 ? "zeropage,y" : "absolute,y";
|
|
40
|
+
if (BRANCHES.has(opcode)) return "relative";
|
|
41
|
+
if (opcode === JMP_INDIRECT) return "(indirect)";
|
|
42
|
+
return length === 2 ? "zeropage" : "absolute";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The operand in 6502 syntax, from the instruction bytes; empty for implied. */
|
|
46
|
+
export function operandText(mode: Mode, bytes: Uint8Array, target: number | undefined): string {
|
|
47
|
+
const byte = `$${(bytes[1] ?? 0).toString(16).padStart(2, "0")}`;
|
|
48
|
+
const word = formatC64Address((bytes[1] ?? 0) | ((bytes[2] ?? 0) << 8));
|
|
49
|
+
switch (mode) {
|
|
50
|
+
case "implied":
|
|
51
|
+
return "";
|
|
52
|
+
case "accumulator":
|
|
53
|
+
return "a";
|
|
54
|
+
case "immediate":
|
|
55
|
+
return `#${byte}`;
|
|
56
|
+
case "zeropage":
|
|
57
|
+
return byte;
|
|
58
|
+
case "zeropage,x":
|
|
59
|
+
return `${byte},x`;
|
|
60
|
+
case "zeropage,y":
|
|
61
|
+
return `${byte},y`;
|
|
62
|
+
case "absolute":
|
|
63
|
+
return word;
|
|
64
|
+
case "absolute,x":
|
|
65
|
+
return `${word},x`;
|
|
66
|
+
case "absolute,y":
|
|
67
|
+
return `${word},y`;
|
|
68
|
+
case "(indirect)":
|
|
69
|
+
return `(${word})`;
|
|
70
|
+
case "(indirect,x)":
|
|
71
|
+
return `(${byte},x)`;
|
|
72
|
+
case "(indirect),y":
|
|
73
|
+
return `(${byte}),y`;
|
|
74
|
+
case "relative":
|
|
75
|
+
return target === undefined ? byte : formatC64Address(target);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ListedInstruction {
|
|
80
|
+
address: string;
|
|
81
|
+
bytes: string;
|
|
82
|
+
mnemonic: string;
|
|
83
|
+
mode: Mode;
|
|
84
|
+
operand: string;
|
|
85
|
+
target?: string;
|
|
86
|
+
executed?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Ghidra's instructions as the list that --instructions writes. */
|
|
90
|
+
export function instructionList(instructions: GhidraInstruction[], coverage?: Uint8Array): ListedInstruction[] {
|
|
91
|
+
return instructions.map((instruction) => {
|
|
92
|
+
const bytes = Buffer.from(instruction.bytes, "hex");
|
|
93
|
+
const mode = modeOf(bytes[0]!, bytes.length, instruction.operands);
|
|
94
|
+
const [target] = instruction.flows;
|
|
95
|
+
return {
|
|
96
|
+
address: formatC64Address(instruction.address),
|
|
97
|
+
bytes: (instruction.bytes.match(/../g) ?? []).join(" "),
|
|
98
|
+
mnemonic: instruction.mnemonic,
|
|
99
|
+
mode,
|
|
100
|
+
operand: operandText(mode, bytes, target),
|
|
101
|
+
...(target === undefined ? {} : { target: formatC64Address(target) }),
|
|
102
|
+
...(coverage === undefined ? {} : { executed: (coverage[instruction.address]! & COVERAGE.ramExecute) !== 0 }),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Macro candidates (D73): runs of instructions that are a known 6502 idiom,
|
|
2
|
+
// such as a 16-bit increment, and that a macro can write as one line. They
|
|
3
|
+
// are only reported, never written: the LLM applies them as macro uses with
|
|
4
|
+
// c64-knowledge, and c64-source writes a use only when it gives the bytes of
|
|
5
|
+
// the image.
|
|
6
|
+
//
|
|
7
|
+
// The catalogue is fixed, so each candidate has a name and a body that is
|
|
8
|
+
// known to be correct. A search for any repeated sequence was weighed and not
|
|
9
|
+
// chosen: it gives many candidates without a name (D73).
|
|
10
|
+
|
|
11
|
+
import { instructionLength } from "#src/c64.ts";
|
|
12
|
+
import type { ImageByte } from "./coverage.ts";
|
|
13
|
+
|
|
14
|
+
/** A macro of the catalogue: its params and its ACME body, which names a param with a dot. */
|
|
15
|
+
export interface MacroIdiom {
|
|
16
|
+
name: string;
|
|
17
|
+
params: string[];
|
|
18
|
+
body: string;
|
|
19
|
+
comment: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const CATALOGUE: readonly MacroIdiom[] = [
|
|
23
|
+
{ name: "inc16", params: ["dst"], body: [" inc .dst", " bne +", " inc .dst+1", "+"].join("\n"), comment: "Adds 1 to the 16-bit value at dst." },
|
|
24
|
+
{ name: "dec16", params: ["dst"], body: [" lda .dst", " bne +", " dec .dst+1", "+ dec .dst"].join("\n"), comment: "Subtracts 1 from the 16-bit value at dst. It changes A." },
|
|
25
|
+
{
|
|
26
|
+
name: "add16_8",
|
|
27
|
+
params: ["dst", "value"],
|
|
28
|
+
body: [" clc", " lda .dst", " adc #.value", " sta .dst", " bcc +", " inc .dst+1", "+"].join("\n"),
|
|
29
|
+
comment: "Adds the 8-bit value to the 16-bit value at dst. It changes A.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "add16",
|
|
33
|
+
params: ["dst", "value"],
|
|
34
|
+
body: [" clc", " lda .dst", " adc #<.value", " sta .dst", " lda .dst+1", " adc #>.value", " sta .dst+1"].join("\n"),
|
|
35
|
+
comment: "Adds the 16-bit value to the 16-bit value at dst. It changes A.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "sub16",
|
|
39
|
+
params: ["dst", "value"],
|
|
40
|
+
body: [" sec", " lda .dst", " sbc #<.value", " sta .dst", " lda .dst+1", " sbc #>.value", " sta .dst+1"].join("\n"),
|
|
41
|
+
comment: "Subtracts the 16-bit value from the 16-bit value at dst. It changes A.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "set16",
|
|
45
|
+
params: ["dst", "value"],
|
|
46
|
+
body: [" lda #<.value", " sta .dst", " lda #>.value", " sta .dst+1"].join("\n"),
|
|
47
|
+
comment: "Puts the 16-bit value at dst. It changes A.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "mov16",
|
|
51
|
+
params: ["dst", "src"],
|
|
52
|
+
body: [" lda .src", " sta .dst", " lda .src+1", " sta .dst+1"].join("\n"),
|
|
53
|
+
comment: "Copies the 16-bit value at src to dst. It changes A.",
|
|
54
|
+
},
|
|
55
|
+
{ name: "wait_line", params: ["line"], body: [" lda #.line", "- cmp $d012", " bne -"].join("\n"), comment: "Waits until the raster line (bits 0-7) is line. It changes A." },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** A run of instructions that one macro of the catalogue can write, with its args as ACME expressions. */
|
|
59
|
+
export interface MacroCandidate {
|
|
60
|
+
macro: string;
|
|
61
|
+
start: number;
|
|
62
|
+
end: number;
|
|
63
|
+
args: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One instruction: its opcode, its length and its operand value. */
|
|
67
|
+
export interface Instruction {
|
|
68
|
+
opcode: number;
|
|
69
|
+
length: number;
|
|
70
|
+
value: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An address operand: zero page or absolute, of the same memory in one idiom. */
|
|
74
|
+
interface Operand {
|
|
75
|
+
value: number;
|
|
76
|
+
absolute: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Opcodes by mnemonic, zero page first, then absolute. */
|
|
80
|
+
const MEMORY = {
|
|
81
|
+
lda: [0xa5, 0xad],
|
|
82
|
+
sta: [0x85, 0x8d],
|
|
83
|
+
inc: [0xe6, 0xee],
|
|
84
|
+
dec: [0xc6, 0xce],
|
|
85
|
+
} as const;
|
|
86
|
+
const IMMEDIATE = { lda: 0xa9, adc: 0x69, sbc: 0xe9 } as const;
|
|
87
|
+
const CLC = 0x18;
|
|
88
|
+
const SEC = 0x38;
|
|
89
|
+
const BNE = 0xd0;
|
|
90
|
+
const BCC = 0x90;
|
|
91
|
+
const CMP_ABSOLUTE = 0xcd;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Names the args of a candidate: `address` gives the name of an address in
|
|
95
|
+
* the knowledge, or undefined. An absolute operand below $0100 keeps four
|
|
96
|
+
* digits, so ACME writes the absolute form again.
|
|
97
|
+
*/
|
|
98
|
+
export type Namer = (address: number) => string | undefined;
|
|
99
|
+
|
|
100
|
+
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
101
|
+
const hex4 = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
102
|
+
|
|
103
|
+
function addressArg(operand: Operand, name: Namer): string {
|
|
104
|
+
const named = name(operand.value);
|
|
105
|
+
// A name of a zero-page address gives the zero-page form: an absolute operand there keeps its number.
|
|
106
|
+
if (named !== undefined && (operand.value >= 0x100 || !operand.absolute)) return named;
|
|
107
|
+
return operand.absolute ? hex4(operand.value) : hex2(operand.value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The memory operand of `instruction` when it is one of the opcodes of the mnemonic. */
|
|
111
|
+
function memory(instruction: Instruction | undefined, mnemonic: keyof typeof MEMORY): Operand | undefined {
|
|
112
|
+
if (instruction === undefined) return undefined;
|
|
113
|
+
const index = (MEMORY[mnemonic] as readonly number[]).indexOf(instruction.opcode);
|
|
114
|
+
return index < 0 ? undefined : { value: instruction.value, absolute: index === 1 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const same = (a: Operand | undefined, b: Operand | undefined, offset = 0): boolean => a !== undefined && b !== undefined && a.absolute === b.absolute && b.value === a.value + offset;
|
|
118
|
+
const immediate = (instruction: Instruction | undefined, opcode: number) => (instruction?.opcode === opcode ? instruction.value : undefined);
|
|
119
|
+
/** A branch past the next instruction only: its distance is the length of that instruction. */
|
|
120
|
+
const skips = (branch: Instruction | undefined, opcode: number, next: Instruction | undefined) => branch?.opcode === opcode && next !== undefined && branch.value === next.length;
|
|
121
|
+
|
|
122
|
+
/** Each idiom: how many instructions it takes, and its args when the instructions match. */
|
|
123
|
+
const MATCHERS: Record<string, { count: number; match: (run: Instruction[], name: Namer) => string[] | undefined }> = {
|
|
124
|
+
inc16: {
|
|
125
|
+
count: 3,
|
|
126
|
+
match: ([low, branch, high], name) => {
|
|
127
|
+
const dst = memory(low, "inc");
|
|
128
|
+
return same(dst, memory(high, "inc"), 1) && skips(branch, BNE, high) ? [addressArg(dst!, name)] : undefined;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
dec16: {
|
|
132
|
+
count: 4,
|
|
133
|
+
match: ([load, branch, high, low], name) => {
|
|
134
|
+
const dst = memory(load, "lda");
|
|
135
|
+
return same(dst, memory(low, "dec")) && same(dst, memory(high, "dec"), 1) && skips(branch, BNE, high) ? [addressArg(dst!, name)] : undefined;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
add16_8: {
|
|
139
|
+
count: 6,
|
|
140
|
+
match: ([clc, load, add, store, branch, high], name) => {
|
|
141
|
+
const dst = memory(load, "lda");
|
|
142
|
+
const value = immediate(add, IMMEDIATE.adc);
|
|
143
|
+
const ok = clc?.opcode === CLC && value !== undefined && same(dst, memory(store, "sta")) && same(dst, memory(high, "inc"), 1) && skips(branch, BCC, high);
|
|
144
|
+
return ok ? [addressArg(dst!, name), hex2(value)] : undefined;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
add16: { count: 7, match: (run, name) => wide(run, name, CLC, IMMEDIATE.adc) },
|
|
148
|
+
sub16: { count: 7, match: (run, name) => wide(run, name, SEC, IMMEDIATE.sbc) },
|
|
149
|
+
set16: {
|
|
150
|
+
count: 4,
|
|
151
|
+
match: ([loadLow, storeLow, loadHigh, storeHigh], name) => {
|
|
152
|
+
const low = immediate(loadLow, IMMEDIATE.lda);
|
|
153
|
+
const high = immediate(loadHigh, IMMEDIATE.lda);
|
|
154
|
+
const dst = memory(storeLow, "sta");
|
|
155
|
+
if (low === undefined || high === undefined || !same(dst, memory(storeHigh, "sta"), 1)) return undefined;
|
|
156
|
+
const value = low | (high << 8);
|
|
157
|
+
return [addressArg(dst!, name), name(value) ?? hex4(value)];
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
mov16: {
|
|
161
|
+
count: 4,
|
|
162
|
+
match: ([loadLow, storeLow, loadHigh, storeHigh], name) => {
|
|
163
|
+
const src = memory(loadLow, "lda");
|
|
164
|
+
const dst = memory(storeLow, "sta");
|
|
165
|
+
if (!same(src, memory(loadHigh, "lda"), 1) || !same(dst, memory(storeHigh, "sta"), 1)) return undefined;
|
|
166
|
+
return [addressArg(dst!, name), addressArg(src!, name)];
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
wait_line: {
|
|
170
|
+
count: 3,
|
|
171
|
+
match: ([load, compare, branch]) => {
|
|
172
|
+
const line = immediate(load, IMMEDIATE.lda);
|
|
173
|
+
// BNE back to the CMP: 5 bytes back from the end of the BNE.
|
|
174
|
+
return line !== undefined && compare?.opcode === CMP_ABSOLUTE && compare.value === 0xd012 && branch?.opcode === BNE && branch.value === 0xfb ? [hex2(line)] : undefined;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** add16 and sub16: the carry, then the low and the high byte, each with an immediate of the 16-bit value. */
|
|
180
|
+
function wide([carry, loadLow, low, storeLow, loadHigh, high, storeHigh]: Instruction[], name: Namer, carryOpcode: number, opcode: number): string[] | undefined {
|
|
181
|
+
const dst = memory(loadLow, "lda");
|
|
182
|
+
const lowValue = immediate(low, opcode);
|
|
183
|
+
const highValue = immediate(high, opcode);
|
|
184
|
+
const ok =
|
|
185
|
+
carry?.opcode === carryOpcode && lowValue !== undefined && highValue !== undefined && same(dst, memory(storeLow, "sta")) && same(dst, memory(loadHigh, "lda"), 1) && same(dst, memory(storeHigh, "sta"), 1);
|
|
186
|
+
return ok ? [addressArg(dst!, name), hex4(lowValue | (highValue << 8))] : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The instruction at `at`, or undefined when a byte of it is outside the range or the image. */
|
|
190
|
+
export function instructionAt(byteAt: ImageByte, at: number, end: number): Instruction | undefined {
|
|
191
|
+
const opcode = byteAt(at);
|
|
192
|
+
if (opcode === undefined) return undefined;
|
|
193
|
+
const length = instructionLength(opcode);
|
|
194
|
+
if (at + length - 1 > end) return undefined;
|
|
195
|
+
const bytes = Array.from({ length: length - 1 }, (_, offset) => byteAt(at + 1 + offset));
|
|
196
|
+
if (bytes.some((byte) => byte === undefined)) return undefined;
|
|
197
|
+
return { opcode, length, value: length === 1 ? 0 : length === 2 ? bytes[0]! : bytes[0]! | (bytes[1]! << 8) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Finds the macro candidates in code regions. Each region is read as
|
|
202
|
+
* instructions from its start; at each instruction the idioms of the
|
|
203
|
+
* catalogue are tried in order, and a match is a candidate. Candidates do not
|
|
204
|
+
* overlap: the scan goes on after the end of a match.
|
|
205
|
+
*/
|
|
206
|
+
export function findMacroUses(byteAt: ImageByte, regions: ReadonlyArray<{ start: number; end: number }>, name: Namer): MacroCandidate[] {
|
|
207
|
+
const candidates: MacroCandidate[] = [];
|
|
208
|
+
for (const region of [...regions].sort((a, b) => a.start - b.start)) {
|
|
209
|
+
const run: Array<Instruction & { at: number }> = [];
|
|
210
|
+
for (let at = region.start; at <= region.end; ) {
|
|
211
|
+
const instruction = instructionAt(byteAt, at, region.end);
|
|
212
|
+
if (instruction === undefined) break;
|
|
213
|
+
run.push({ ...instruction, at });
|
|
214
|
+
at += instruction.length;
|
|
215
|
+
}
|
|
216
|
+
for (let index = 0; index < run.length; ) {
|
|
217
|
+
const found = CATALOGUE.flatMap((idiom) => {
|
|
218
|
+
const matcher = MATCHERS[idiom.name]!;
|
|
219
|
+
const part = run.slice(index, index + matcher.count);
|
|
220
|
+
const args = part.length === matcher.count ? matcher.match(part, name) : undefined;
|
|
221
|
+
return args === undefined ? [] : [{ idiom, count: matcher.count, args }];
|
|
222
|
+
})[0];
|
|
223
|
+
if (found === undefined) {
|
|
224
|
+
index++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const last = run[index + found.count - 1]!;
|
|
228
|
+
candidates.push({ macro: found.idiom.name, start: run[index]!.at, end: last.at + last.length - 1, args: found.args });
|
|
229
|
+
index += found.count;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return candidates;
|
|
233
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Platform pitfalls in code regions (M23.7): code that a C64 runs in another
|
|
2
|
+
// way than it reads. A fixed catalogue, reported as candidates only (D51).
|
|
3
|
+
//
|
|
4
|
+
// raster-bit-8: a read of $D011 gives bit 8 of the current raster line in
|
|
5
|
+
// bit 7. Code that writes the value back without clearing bit 7 sets bit 8 of
|
|
6
|
+
// the raster compare: the next raster interrupt then comes at line 256 or
|
|
7
|
+
// later, or never on a PAL machine past line 311 (the Bruce Lee freeze).
|
|
8
|
+
|
|
9
|
+
import type { ImageByte } from "./coverage.ts";
|
|
10
|
+
import { instructionAt, type Instruction } from "./macros.ts";
|
|
11
|
+
|
|
12
|
+
export interface Pitfall {
|
|
13
|
+
kind: "raster-bit-8";
|
|
14
|
+
/** The instruction that reads $D011. */
|
|
15
|
+
at: number;
|
|
16
|
+
/** The instruction that writes the value back to $D011. */
|
|
17
|
+
store: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const D011 = 0xd011;
|
|
21
|
+
/** LDA, LDX and LDY absolute, and the stores of the same register. */
|
|
22
|
+
const LOADS: Record<number, "a" | "x" | "y"> = { 0xad: "a", 0xae: "x", 0xac: "y" };
|
|
23
|
+
const STORES: Record<number, "a" | "x" | "y"> = { 0x8d: "a", 0x8e: "x", 0x8c: "y" };
|
|
24
|
+
/** INC, DEC, ASL, ROL and ROR absolute: they read the register and write it back. LSR shifts a 0 into bit 7. */
|
|
25
|
+
const READ_MODIFY_WRITE = new Set([0xee, 0xce, 0x0e, 0x2e, 0x6e]);
|
|
26
|
+
/** Instructions after which the next one is not the next in the flow: a jump, a return, a call or a branch. */
|
|
27
|
+
const FLOW = new Set([0x4c, 0x6c, 0x20, 0x60, 0x40, 0x00, 0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
|
|
28
|
+
/** Instructions that load A, X or Y with a new value, which ends what the load of $D011 gave. */
|
|
29
|
+
const NEW_VALUE: Record<"a" | "x" | "y", Set<number>> = {
|
|
30
|
+
a: new Set([0xa9, 0xa5, 0xb5, 0xad, 0xbd, 0xb9, 0xa1, 0xb1, 0x8a, 0x98, 0x68]),
|
|
31
|
+
x: new Set([0xa2, 0xa6, 0xb6, 0xae, 0xbe, 0xaa, 0xba]),
|
|
32
|
+
y: new Set([0xa0, 0xa4, 0xb4, 0xac, 0xbc, 0xa8]),
|
|
33
|
+
};
|
|
34
|
+
/** The most instructions between the load and the store. */
|
|
35
|
+
const WINDOW = 8;
|
|
36
|
+
|
|
37
|
+
/** The raster-bit-8 pitfalls in the code regions, in address order. */
|
|
38
|
+
export function findPitfalls(byteAt: ImageByte, regions: ReadonlyArray<{ start: number; end: number }>): Pitfall[] {
|
|
39
|
+
const found: Pitfall[] = [];
|
|
40
|
+
for (const region of [...regions].sort((a, b) => a.start - b.start)) {
|
|
41
|
+
const run: Array<Instruction & { at: number }> = [];
|
|
42
|
+
for (let at = region.start; at <= region.end; ) {
|
|
43
|
+
const instruction = instructionAt(byteAt, at, region.end);
|
|
44
|
+
if (instruction === undefined) break;
|
|
45
|
+
run.push({ ...instruction, at });
|
|
46
|
+
at += instruction.length;
|
|
47
|
+
}
|
|
48
|
+
run.forEach((instruction, index) => {
|
|
49
|
+
if (instruction.length !== 3 || instruction.value !== D011) return;
|
|
50
|
+
if (READ_MODIFY_WRITE.has(instruction.opcode)) {
|
|
51
|
+
found.push({ kind: "raster-bit-8", at: instruction.at, store: instruction.at });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const register = LOADS[instruction.opcode];
|
|
55
|
+
if (register === undefined) return;
|
|
56
|
+
for (const next of run.slice(index + 1, index + 1 + WINDOW)) {
|
|
57
|
+
// AND #imm without bit 7 clears raster bit 8: the code knows about it.
|
|
58
|
+
if (register === "a" && next.opcode === 0x29 && (next.value & 0x80) === 0) return;
|
|
59
|
+
if (STORES[next.opcode] === register && next.length === 3 && next.value === D011) {
|
|
60
|
+
found.push({ kind: "raster-bit-8", at: instruction.at, store: next.at });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (FLOW.has(next.opcode) || NEW_VALUE[register].has(next.opcode)) return;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return found;
|
|
68
|
+
}
|