@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.8
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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +1 -1
- package/skills/c64-basic/SKILL.md +2 -2
- package/skills/c64-basic/scripts/basic.ts +1 -1
- package/skills/c64-disk/SKILL.md +3 -3
- package/skills/c64-disk/scripts/disk.ts +2 -1
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +2 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-provenance/SKILL.md +10 -5
- package/skills/c64-provenance/scripts/compare.ts +35 -6
- package/skills/c64-provenance/scripts/provenance.ts +35 -15
- package/skills/c64-provenance/scripts/separate.ts +12 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -2
- package/skills/c64-source/SKILL.md +18 -11
- package/skills/c64-source/scripts/generate.ts +55 -10
- package/skills/c64-source/scripts/loader.ts +253 -6
- package/skills/c64-source/scripts/source.ts +46 -11
- package/skills/c64-static-analysis/SKILL.md +4 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
- package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
- package/skills/c64-testing/SKILL.md +1 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +9 -7
- package/skills/c64-unpacker/SKILL.md +11 -9
- package/skills/c64-unpacker/scripts/loads.ts +16 -4
- package/skills/c64-unpacker/scripts/machine.ts +13 -1
- package/skills/c64-unpacker/scripts/stages.ts +66 -3
- package/skills/c64-unpacker/scripts/unpack.ts +135 -42
- package/src/c64.ts +19 -5
- package/src/cli/status.ts +26 -3
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/petcat.ts +5 -2
- package/src/host/tools/status.ts +6 -3
- package/src/host/vice/adapter.ts +37 -9
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +7 -2
- package/src/host/vice/screen.ts +3 -2
- package/src/host/vice/session.ts +68 -15
- package/src/host-client/autostart.ts +11 -1
- package/src/host-client/connect.ts +10 -10
- package/src/host-client/coverage.ts +19 -10
- package/src/host-client/tools.ts +6 -6
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/schema.ts +7 -0
- package/src/knowledge/write.ts +5 -0
- package/src/mcp/tools/debug.ts +9 -5
- package/src/mcp/tools/machine.ts +4 -1
- package/src/mcp/tools/media.ts +7 -1
- package/src/mcp/tools/video.ts +7 -2
- package/src/native/discover.ts +2 -1
- package/src/platform.ts +3 -2
- package/src/protocol/tools.ts +19 -4
- package/src/protocol/vice.ts +56 -5
|
@@ -25,9 +25,9 @@ import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/nativ
|
|
|
25
25
|
import { platformNames } from "#src/platform.ts";
|
|
26
26
|
import { resolveProjectPath } from "#src/project.ts";
|
|
27
27
|
import { runScript, UsageError } from "#src/script.ts";
|
|
28
|
-
import { LOADER_JUMP, planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
28
|
+
import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
29
29
|
import { OPCODES } from "./opcodes.ts";
|
|
30
|
-
import { instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
|
|
30
|
+
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
|
|
31
31
|
|
|
32
32
|
const USAGE = `generate.ts <image> --out <dir> [options]
|
|
33
33
|
|
|
@@ -36,7 +36,8 @@ const USAGE = `generate.ts <image> --out <dir> [options]
|
|
|
36
36
|
--out <dir> the directory for the source files, relative to the project
|
|
37
37
|
--entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
|
|
38
38
|
--coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
|
|
39
|
-
that no run executed is marked and listed
|
|
39
|
+
that no run executed is marked and listed; <dir>/not_executed.json
|
|
40
|
+
has all of its ranges
|
|
40
41
|
--space <space> the knowledge space of the image: c64 (the default), drive8, or
|
|
41
42
|
bank0 to bank255; --coverage and --loader are for c64 only
|
|
42
43
|
--loader also write loader.asm and loader.prg: the loader copies the
|
|
@@ -129,6 +130,8 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
|
|
|
129
130
|
}
|
|
130
131
|
const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
|
|
131
132
|
if (entryPoints.length === 0) {
|
|
133
|
+
// An entry in no segment is a capture fault: the image is data only, and the result says entryOutside (R3-01).
|
|
134
|
+
if (image.entry !== undefined && !inImage(image.entry)) return new Map();
|
|
132
135
|
throw new UsageError("Ghidra needs an entry point: give --entry, or record a routine or a code region in knowledge with the c64-knowledge skill");
|
|
133
136
|
}
|
|
134
137
|
const dataRanges = knowledge.regions
|
|
@@ -276,6 +279,30 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
276
279
|
differ("sp", registers.sp, state.sp);
|
|
277
280
|
const flags = (["n", "v", "d", "i", "z", "c"] as const).reduce((value, name, index) => value | (registers.flags[name] ? [0x80, 0x40, 0x08, 0x04, 0x02, 0x01][index]! : 0), 0);
|
|
278
281
|
differ("flags", flags, state.flags & 0xcf);
|
|
282
|
+
if (state.vic !== undefined) {
|
|
283
|
+
// The display registers and the raster line (D60): the loader waited for the line, so the jump comes in it or the next one.
|
|
284
|
+
const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
|
|
285
|
+
for (const { register, mask } of VIC_DISPLAY_REGISTERS) differ(formatC64Address(0xd000 + register), vic[register]! & mask, state.vic.registers[register]! & mask);
|
|
286
|
+
differ("the VIC bank", Buffer.from((await client.ioRead(0xdd00, 1)).data, "hex")[0]! & 0x03, state.vic.bank);
|
|
287
|
+
const line = rasterLine(vic);
|
|
288
|
+
const wanted = rasterLine(state.vic.registers);
|
|
289
|
+
if (line !== wanted && line !== wanted + 1) differences.push(`the raster line is ${line}, not ${wanted}`);
|
|
290
|
+
}
|
|
291
|
+
if (plan.cia !== undefined) {
|
|
292
|
+
// The CIA timers (D63): at the jump a running timer is 3 cycles, the JMP, before its value at the entry.
|
|
293
|
+
const chips = (await client.cia("both")).chips;
|
|
294
|
+
const word = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
295
|
+
const differWord = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${word(got)}, not ${word(wanted)}`);
|
|
296
|
+
for (const timer of plan.cia.timers) {
|
|
297
|
+
const chip = chips[timer.cia - 1]!;
|
|
298
|
+
const name = `CIA ${timer.cia} timer ${timer.timer}`;
|
|
299
|
+
const [counter, latch, control] = timer.timer === "A" ? [chip.timerA, chip.latchA, chip.controlA] : [chip.timerB, chip.latchB, chip.controlB];
|
|
300
|
+
differ(`${name} control`, control, timer.control);
|
|
301
|
+
differWord(`${name} latch`, latch, timer.latch);
|
|
302
|
+
if (timer.counterAtJump !== undefined) differWord(`${name} counter`, counter, timer.counterAtJump);
|
|
303
|
+
}
|
|
304
|
+
plan.cia.masks.forEach((mask, index) => differ(`CIA ${index + 1} interrupt mask`, chips[index]!.interruptMask, mask));
|
|
305
|
+
}
|
|
279
306
|
return { verified: differences.length === 0, differences: differences.slice(0, MAX_LISTED) };
|
|
280
307
|
} finally {
|
|
281
308
|
await client.close();
|
|
@@ -284,8 +311,10 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
284
311
|
|
|
285
312
|
/** The first words of every file that the script writes. */
|
|
286
313
|
const GENERATED_MARK = "; Generated by c64-source";
|
|
314
|
+
/** The file with every range of instructions that no run executed (D49). */
|
|
315
|
+
const NOT_EXECUTED_FILE = "not_executed.json";
|
|
287
316
|
|
|
288
|
-
/** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg; other files stay. */
|
|
317
|
+
/** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg and not_executed.json; other files stay. */
|
|
289
318
|
function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
|
|
290
319
|
const directory = resolveProjectPath(out);
|
|
291
320
|
if (!existsSync(directory)) return;
|
|
@@ -293,7 +322,7 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
|
|
|
293
322
|
if (!entry.isFile()) continue;
|
|
294
323
|
const name = entry.name;
|
|
295
324
|
const path = join(directory, name);
|
|
296
|
-
if (name === "loader.prg") rmSync(path, { force: true });
|
|
325
|
+
if (name === "loader.prg" || name === NOT_EXECUTED_FILE) rmSync(path, { force: true });
|
|
297
326
|
else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
|
|
298
327
|
}
|
|
299
328
|
}
|
|
@@ -344,16 +373,18 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
344
373
|
referenceTargets: knowledge.references.map((reference) => reference.to),
|
|
345
374
|
...(coverage === undefined ? {} : { coverage }),
|
|
346
375
|
// The platform names are C64 names: drive code and banks get none.
|
|
347
|
-
...(space === DEFAULT_SPACE ? { platform:
|
|
376
|
+
...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
|
|
348
377
|
});
|
|
349
378
|
const asBytes = await probe(laid);
|
|
350
379
|
const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
|
|
351
380
|
const rendered = render(laid, asBytes, header);
|
|
352
381
|
const files = [...rendered.files];
|
|
353
382
|
let loader: LoaderPlan | undefined;
|
|
383
|
+
// An image without its entry has no program: the source is still written and checked, but the run fails (R2-02).
|
|
384
|
+
const missingEntry = entryOutsideImage(image.entry, image.segments);
|
|
354
385
|
// A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
|
|
355
|
-
let loaderRefused: string | undefined;
|
|
356
|
-
if (state !== undefined) {
|
|
386
|
+
let loaderRefused: string | undefined = state === undefined ? undefined : missingEntry;
|
|
387
|
+
if (state !== undefined && missingEntry === undefined) {
|
|
357
388
|
const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
|
|
358
389
|
try {
|
|
359
390
|
loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
|
|
@@ -367,6 +398,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
367
398
|
for (const [index, file] of files.entries()) if (!file.text.startsWith(GENERATED_MARK)) files[index] = { ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` };
|
|
368
399
|
removeOldFiles(values.out, new Set(files.map((file) => file.name)));
|
|
369
400
|
for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
|
|
401
|
+
const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
402
|
+
const inData = entriesInData(entries, knowledge.regions);
|
|
403
|
+
if (rendered.notExecuted !== undefined) {
|
|
404
|
+
const all = { image: image.path, revision: knowledge.revision, instructions: rendered.notExecuted.instructions, ranges: rendered.notExecuted.ranges.map(showRange) };
|
|
405
|
+
writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
|
|
406
|
+
}
|
|
370
407
|
|
|
371
408
|
const output: Record<string, unknown> = {
|
|
372
409
|
out: values.out,
|
|
@@ -375,7 +412,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
375
412
|
instructions: rendered.instructions,
|
|
376
413
|
dataBytes: rendered.dataBytes,
|
|
377
414
|
...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
|
|
415
|
+
...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
|
|
378
416
|
labels: { named: laid.namedLabels, generated: laid.generatedLabels },
|
|
417
|
+
// An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it (F-27).
|
|
418
|
+
...(inData.length === 0
|
|
419
|
+
? {}
|
|
420
|
+
: { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
|
|
379
421
|
...(rendered.asBytes.length === 0
|
|
380
422
|
? {}
|
|
381
423
|
: { asBytes: { count: rendered.asBytes.length, listed: rendered.asBytes.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), instruction: item.text })) } }),
|
|
@@ -385,7 +427,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
385
427
|
notExecuted: {
|
|
386
428
|
instructions: rendered.notExecuted.instructions,
|
|
387
429
|
ranges: rendered.notExecuted.ranges.length,
|
|
388
|
-
listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(
|
|
430
|
+
listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(showRange),
|
|
431
|
+
all: `${values.out}/${NOT_EXECUTED_FILE}`,
|
|
389
432
|
},
|
|
390
433
|
}),
|
|
391
434
|
...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
|
|
@@ -400,7 +443,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
400
443
|
output.check = checked;
|
|
401
444
|
if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
|
|
402
445
|
if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
|
|
403
|
-
if (loader === undefined || state === undefined) return { output, failed:
|
|
446
|
+
if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
|
|
404
447
|
|
|
405
448
|
const loaderBuilt = await assembleFiles(files, "loader.asm");
|
|
406
449
|
if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
|
|
@@ -431,6 +474,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
431
474
|
copier: formatC64Address(loader.copier),
|
|
432
475
|
stackByte: formatC64Address(loader.stackByte),
|
|
433
476
|
...ran,
|
|
477
|
+
// What the loader cannot set as it was at the entry (D63).
|
|
478
|
+
...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
|
|
434
479
|
};
|
|
435
480
|
return { output, failed: !ran.verified };
|
|
436
481
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The loader of a generated source (D48): a PRG that copies each segment and
|
|
2
|
-
// the entry state's pages $0002-$03ff into place, sets
|
|
2
|
+
// the entry state's pages $0002-$03ff into place, sets the display registers
|
|
3
|
+
// of the VIC-II and the VIC bank, waits for the entry's raster line (D60),
|
|
4
|
+
// sets the CIA timers and interrupt masks (D63), sets $00/$01, the stack
|
|
3
5
|
// pointer, A, X, Y and the flags, and jumps to the entry. The copies run
|
|
4
6
|
// from a free page with SEI and all RAM visible, in an order where no copy
|
|
5
7
|
// overwrites the packed bytes of a copy that has not run yet. The plan is
|
|
@@ -20,6 +22,41 @@ export interface EntryState {
|
|
|
20
22
|
port: [number, number];
|
|
21
23
|
/** $0000-$03ff at the entry. */
|
|
22
24
|
pages: Uint8Array;
|
|
25
|
+
/** The VIC-II at the entry (D60): $d000-$d02e, and the VIC bank (bits 0-1 of $dd00). */
|
|
26
|
+
vic?: { registers: Uint8Array; bank: number };
|
|
27
|
+
/** CIA 1 and CIA 2 at the entry (D63). */
|
|
28
|
+
cia?: [EntryCia, EntryCia];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A CIA at the entry: $xx00-$xx0f as a read without side effects gives them, and what no read gives. */
|
|
32
|
+
export interface EntryCia {
|
|
33
|
+
registers: Uint8Array;
|
|
34
|
+
latchA: number;
|
|
35
|
+
latchB: number;
|
|
36
|
+
interruptMask: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The VIC-II registers that the loader sets (D60), each with the bits that a
|
|
41
|
+
* read gives back: not the raster, the light pen, the interrupt registers or
|
|
42
|
+
* the collisions. The raster line comes from a wait instead.
|
|
43
|
+
*/
|
|
44
|
+
export const VIC_DISPLAY_REGISTERS: ReadonlyArray<{ register: number; mask: number }> = [
|
|
45
|
+
...Array.from({ length: 0x11 }, (_, register) => ({ register, mask: 0xff })),
|
|
46
|
+
{ register: 0x11, mask: 0x7f },
|
|
47
|
+
{ register: 0x15, mask: 0xff },
|
|
48
|
+
{ register: 0x16, mask: 0x3f },
|
|
49
|
+
{ register: 0x17, mask: 0xff },
|
|
50
|
+
{ register: 0x18, mask: 0xfe },
|
|
51
|
+
{ register: 0x1b, mask: 0xff },
|
|
52
|
+
{ register: 0x1c, mask: 0xff },
|
|
53
|
+
{ register: 0x1d, mask: 0xff },
|
|
54
|
+
...Array.from({ length: 0x0f }, (_, index) => ({ register: 0x20 + index, mask: 0x0f })),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** The raster line of the entry, from $d011 bit 7 and $d012 as the capture read them. */
|
|
58
|
+
export function rasterLine(registers: Uint8Array): number {
|
|
59
|
+
return ((registers[0x11]! & 0x80) << 1) | registers[0x12]!;
|
|
23
60
|
}
|
|
24
61
|
|
|
25
62
|
const FLAG_BITS: Record<string, number> = { n: 0x80, v: 0x40, b: 0x10, d: 0x08, i: 0x04, z: 0x02, c: 0x01 };
|
|
@@ -57,6 +94,8 @@ export function readEntryState(text: string, path: string): EntryState {
|
|
|
57
94
|
flags,
|
|
58
95
|
port: [byte(value.cpuPort?.$00, "cpuPort.$00"), byte(value.cpuPort?.$01, "cpuPort.$01")],
|
|
59
96
|
pages,
|
|
97
|
+
...readVic(value.io, fail),
|
|
98
|
+
...readCia(value, fail),
|
|
60
99
|
};
|
|
61
100
|
}
|
|
62
101
|
|
|
@@ -86,6 +125,8 @@ export interface LoaderPlan {
|
|
|
86
125
|
stackByte: number;
|
|
87
126
|
end: number;
|
|
88
127
|
text: string;
|
|
128
|
+
/** The CIA timers and masks that the loader sets, when the entry state has them (D63). */
|
|
129
|
+
cia?: CiaPlan;
|
|
89
130
|
}
|
|
90
131
|
|
|
91
132
|
/** The label of the loader's jump to the entry, in ACME's symbol list. */
|
|
@@ -97,9 +138,14 @@ const STUB = 0x080d;
|
|
|
97
138
|
const STUB_SIZE = 21;
|
|
98
139
|
/** The copier code and its three variables, without the table. */
|
|
99
140
|
const COPIER_CODE_SIZE = 167;
|
|
141
|
+
/** The VIC-II code of the copier (D60), without its two tables of one byte per register. */
|
|
142
|
+
const VIC_CODE_SIZE = 44;
|
|
143
|
+
const VIC_SIZE = VIC_CODE_SIZE + 2 * VIC_DISPLAY_REGISTERS.length;
|
|
144
|
+
/** The copier and the stub's move of it: two pages. */
|
|
145
|
+
const COPIER_LIMIT = 0x200;
|
|
100
146
|
const COPY_ENTRY = 7;
|
|
101
|
-
/** X indexes the table, so the table fits in 256 bytes. */
|
|
102
|
-
export const MAX_COPIES = Math.floor((256 - 2) / COPY_ENTRY);
|
|
147
|
+
/** X indexes the table, so the table fits in 256 bytes, and the copier with the VIC-II code fits in two pages. */
|
|
148
|
+
export const MAX_COPIES = Math.min(Math.floor((256 - 2) / COPY_ENTRY), Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - VIC_SIZE - 2) / COPY_ENTRY));
|
|
103
149
|
/** A loader that loads into $d000 or higher would write the I/O area. */
|
|
104
150
|
const LOAD_LIMIT = 0xcfff;
|
|
105
151
|
const COPIER_PAGES = [...Array.from({ length: 15 }, (_, index) => 0xc000 + index * 0x100), ...Array.from({ length: 0x9b }, (_, index) => 0x9e00 - index * 0x100)];
|
|
@@ -147,6 +193,161 @@ export function simulate(memory: Int16Array, copies: Copy[], expected: (address:
|
|
|
147
193
|
return undefined;
|
|
148
194
|
}
|
|
149
195
|
|
|
196
|
+
/** The VIC-II and its bank from the io part of an entry state, when it has them. */
|
|
197
|
+
function readVic(io: Record<string, any> | undefined, fail: (what: string) => never): { vic: NonNullable<EntryState["vic"]> } | Record<string, never> {
|
|
198
|
+
if (io?.vic === undefined) return {};
|
|
199
|
+
const vic = io.vic.bytes;
|
|
200
|
+
const cia2 = io.cia2?.bytes;
|
|
201
|
+
if (typeof vic !== "string" || !/^[0-9a-f]{94}$/i.test(vic)) return fail("io.vic.bytes is not 47 bytes of hex");
|
|
202
|
+
if (typeof cia2 !== "string" || !/^[0-9a-f]{2}/i.test(cia2)) return fail("io.cia2.bytes is missing");
|
|
203
|
+
return { vic: { registers: Buffer.from(vic, "hex"), bank: Number.parseInt(cia2.slice(0, 2), 16) & 0x03 } };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readCia(value: Record<string, any>, fail: (what: string) => never): { cia: [EntryCia, EntryCia] } | Record<string, never> {
|
|
207
|
+
if (value.cia === undefined) return {};
|
|
208
|
+
const chip = (name: "cia1" | "cia2"): EntryCia => {
|
|
209
|
+
const bytes = value.io?.[name]?.bytes;
|
|
210
|
+
if (typeof bytes !== "string" || !/^[0-9a-f]{32}$/i.test(bytes)) return fail(`io.${name}.bytes is not 16 bytes of hex`);
|
|
211
|
+
const hidden = value.cia[name];
|
|
212
|
+
const number = (item: unknown, what: string, max: number) => (Number.isInteger(item) && (item as number) >= 0 && (item as number) <= max ? (item as number) : fail(`cia.${name}.${what} is not a number from 0 to ${max}`));
|
|
213
|
+
return { registers: Buffer.from(bytes, "hex"), latchA: number(hidden?.latchA, "latchA", 0xffff), latchB: number(hidden?.latchB, "latchB", 0xffff), interruptMask: number(hidden?.interruptMask, "interruptMask", 0x1f) };
|
|
214
|
+
};
|
|
215
|
+
return { cia: [chip("cia1"), chip("cia2")] };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** One CIA timer that the loader sets, and what the check of the loader expects of it at the jump. */
|
|
219
|
+
export interface CiaTimerPlan {
|
|
220
|
+
cia: 1 | 2;
|
|
221
|
+
timer: "A" | "B";
|
|
222
|
+
/** The control register as a read gives it: without bit 4, the force load. */
|
|
223
|
+
control: number;
|
|
224
|
+
latch: number;
|
|
225
|
+
/** The counter at the jump, 3 cycles before the entry; undefined when the loader cannot set it. */
|
|
226
|
+
counterAtJump?: number;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface CiaPlan {
|
|
230
|
+
timers: CiaTimerPlan[];
|
|
231
|
+
masks: [number, number];
|
|
232
|
+
/** What the loader cannot set as it was at the entry. */
|
|
233
|
+
limits: string[];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The CIA timers as the KERNAL leaves them when RUN starts the loader, on a
|
|
238
|
+
* PAL C64: only CIA 1 timer A runs, for the 60 Hz interrupt. A timer that the
|
|
239
|
+
* entry has in this state is not written. The loader check reads every timer.
|
|
240
|
+
*/
|
|
241
|
+
const KERNAL_TIMERS: ReadonlyArray<{ control: number; latch: number; counter?: number }> = [
|
|
242
|
+
{ control: 0x01, latch: 0x4025 },
|
|
243
|
+
{ control: 0x08, latch: 0xffff, counter: 0xffff },
|
|
244
|
+
{ control: 0x08, latch: 0xffff, counter: 0xffff },
|
|
245
|
+
{ control: 0x08, latch: 0xffff, counter: 0xffff },
|
|
246
|
+
];
|
|
247
|
+
const KERNAL_MASKS: readonly [number, number] = [0x01, 0x00];
|
|
248
|
+
/** Cycles from the end of the CIA part to the entry: $00, $01, the stack pointer, the flags, A, X, Y, PLP and JMP. */
|
|
249
|
+
const TAIL_CYCLES = 33;
|
|
250
|
+
/** Cycles after the write that starts a CIA timer in which it does not count yet, measured in VICE. */
|
|
251
|
+
const CIA_START_DELAY = 1;
|
|
252
|
+
/** Cycles of one raster line of a PAL C64. */
|
|
253
|
+
const LINE_CYCLES = 63;
|
|
254
|
+
const PAL_LINES = 312;
|
|
255
|
+
|
|
256
|
+
interface Op {
|
|
257
|
+
text: string;
|
|
258
|
+
bytes: number;
|
|
259
|
+
cycles: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The copier's CIA part (D63), after the raster wait: the timers and
|
|
264
|
+
* interrupt masks of the entry state. A running timer is stopped, loaded
|
|
265
|
+
* with its counter at the entry plus the cycles to the entry, started, and
|
|
266
|
+
* given its latch, so it counts to the entry's value just at the entry.
|
|
267
|
+
* Then the interrupt flags that the copy left are read away.
|
|
268
|
+
*/
|
|
269
|
+
export function ciaSection(state: EntryState): { ops: Op[]; plan: CiaPlan } | undefined {
|
|
270
|
+
if (state.cia === undefined) return undefined;
|
|
271
|
+
const lda = (value: number): Op => ({ text: `lda #${hex2(value)}`, bytes: 2, cycles: 2 });
|
|
272
|
+
const sta = (address: number): Op => ({ text: `sta ${hex4(address)}`, bytes: 3, cycles: 4 });
|
|
273
|
+
const limits: string[] = [];
|
|
274
|
+
const timers: CiaTimerPlan[] = [];
|
|
275
|
+
const disable: Op[] = [];
|
|
276
|
+
const loads: Array<Op | ((cycles: number[]) => Op)> = [];
|
|
277
|
+
const starts: Op[] = [];
|
|
278
|
+
const latches: Op[] = [];
|
|
279
|
+
const acks: Op[] = [];
|
|
280
|
+
const masks: Op[] = [];
|
|
281
|
+
/** For each running timer: its index in starts, and how its load value follows from the cycles after its start. */
|
|
282
|
+
const running: Array<{ start: number; counter: number; latch: number; system: boolean; name: string; plan: CiaTimerPlan }> = [];
|
|
283
|
+
state.cia.forEach((chip, index) => {
|
|
284
|
+
const id = (index + 1) as 1 | 2;
|
|
285
|
+
const base = index === 0 ? 0xdc00 : 0xdd00;
|
|
286
|
+
let touched = false;
|
|
287
|
+
for (const timer of ["A", "B"] as const) {
|
|
288
|
+
const at = timer === "A" ? 0x04 : 0x06;
|
|
289
|
+
const controlAt = base + (timer === "A" ? 0x0e : 0x0f);
|
|
290
|
+
const control = chip.registers[timer === "A" ? 0x0e : 0x0f]! & 0xef;
|
|
291
|
+
const counter = chip.registers[at]! | (chip.registers[at + 1]! << 8);
|
|
292
|
+
const latch = timer === "A" ? chip.latchA : chip.latchB;
|
|
293
|
+
const name = `CIA ${id} timer ${timer}`;
|
|
294
|
+
const plan: CiaTimerPlan = { cia: id, timer, control, latch, counterAtJump: counter };
|
|
295
|
+
timers.push(plan);
|
|
296
|
+
const kernal = KERNAL_TIMERS[index * 2 + (timer === "A" ? 0 : 1)]!;
|
|
297
|
+
const runs = (control & 0x01) !== 0;
|
|
298
|
+
if (!runs && control === kernal.control && latch === kernal.latch && counter === kernal.counter) continue;
|
|
299
|
+
touched = true;
|
|
300
|
+
// Stopped, a write of the high latch byte also loads the counter.
|
|
301
|
+
loads.push(lda(control & 0xee), sta(controlAt));
|
|
302
|
+
if (runs) {
|
|
303
|
+
// Timer A counts system cycles without bit 5, timer B without bits 5 and 6; other inputs do not count in the loader.
|
|
304
|
+
const system = timer === "A" ? (control & 0x20) === 0 : (control & 0x60) === 0;
|
|
305
|
+
const slot = running.length;
|
|
306
|
+
running.push({ start: starts.length, counter, latch, system, name, plan });
|
|
307
|
+
loads.push((values) => lda(values[slot]! & 0xff), sta(base + at), (values) => lda(values[slot]! >> 8), sta(base + at + 1));
|
|
308
|
+
starts.push(lda(control), sta(controlAt));
|
|
309
|
+
latches.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
|
|
310
|
+
if (system) plan.counterAtJump = counter + 3;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
loads.push(lda(counter & 0xff), sta(base + at), lda(counter >> 8), sta(base + at + 1));
|
|
314
|
+
if (latch === counter) continue;
|
|
315
|
+
if (latch >> 8 === counter >> 8) {
|
|
316
|
+
loads.push(lda(latch & 0xff), sta(base + at));
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// The high byte loads the counter of a stopped timer too.
|
|
320
|
+
loads.push(lda(latch & 0xff), sta(base + at), lda(latch >> 8), sta(base + at + 1));
|
|
321
|
+
plan.counterAtJump = latch;
|
|
322
|
+
limits.push(`${name} is stopped with the counter $${counter.toString(16).padStart(4, "0")} and the latch $${latch.toString(16).padStart(4, "0")}: a stopped CIA timer loads its latch, so the counter is the latch.`);
|
|
323
|
+
}
|
|
324
|
+
const mask = chip.interruptMask;
|
|
325
|
+
if (mask !== KERNAL_MASKS[index]) {
|
|
326
|
+
disable.push(lda(0x7f), sta(base + 0x0d));
|
|
327
|
+
if (mask !== 0) masks.push(lda(0x80 | mask), sta(base + 0x0d));
|
|
328
|
+
touched = true;
|
|
329
|
+
}
|
|
330
|
+
if (touched) acks.push({ text: `lda ${hex4(base + 0x0d)}`, bytes: 3, cycles: 4 });
|
|
331
|
+
const flags = chip.registers[0x0d]! & 0x1f;
|
|
332
|
+
if (flags !== 0) limits.push(`CIA ${id} has the interrupt flags $${flags.toString(16).padStart(2, "0")} at the entry: only an event sets a flag, so the loader cannot.`);
|
|
333
|
+
});
|
|
334
|
+
if (loads.length === 0 && disable.length === 0) return { ops: [], plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
|
|
335
|
+
// The cycles from each start write to the entry: what follows it here, and the tail.
|
|
336
|
+
const after = [...starts, ...latches, ...acks, ...masks];
|
|
337
|
+
const values = running.map((item) => {
|
|
338
|
+
const followers = after.slice(item.start + 2).reduce((sum, op) => sum + op.cycles, 0);
|
|
339
|
+
if (!item.system) return item.counter;
|
|
340
|
+
const load = item.counter + followers + TAIL_CYCLES - CIA_START_DELAY;
|
|
341
|
+
if (load <= 0xffff) return load;
|
|
342
|
+
limits.push(`${item.name} counts from $${item.counter.toString(16).padStart(4, "0")} at the entry: the loader cannot start it so near $ffff without an underflow, so the counter differs.`);
|
|
343
|
+
delete item.plan.counterAtJump;
|
|
344
|
+
return item.counter;
|
|
345
|
+
});
|
|
346
|
+
const pre: Op[] = state.vic === undefined ? [lda(0x35), { text: "sta $01", bytes: 2, cycles: 3 }] : [];
|
|
347
|
+
const ops = [...pre, ...disable, ...loads.map((op) => (typeof op === "function" ? op(values) : op)), ...after];
|
|
348
|
+
return { ops, plan: { timers, masks: [state.cia[0].interruptMask, state.cia[1].interruptMask], limits } };
|
|
349
|
+
}
|
|
350
|
+
|
|
150
351
|
export interface LoaderInput {
|
|
151
352
|
/** The segments with the files that hold them, in address order. */
|
|
152
353
|
segments: Array<Range & { files: string[] }>;
|
|
@@ -179,9 +380,13 @@ export function planLoader(input: LoaderInput): LoaderPlan {
|
|
|
179
380
|
}
|
|
180
381
|
at += block.range.end - block.range.start + 1;
|
|
181
382
|
}
|
|
182
|
-
|
|
383
|
+
const cia = ciaSection(state);
|
|
384
|
+
const ciaSize = cia === undefined ? 0 : cia.ops.reduce((sum, op) => sum + op.bytes, 0);
|
|
385
|
+
// The CIA part takes room from the copy table in the copier's two pages.
|
|
386
|
+
const maxCopies = Math.min(MAX_COPIES, Math.floor((COPIER_LIMIT - COPIER_CODE_SIZE - (state.vic === undefined ? 0 : VIC_SIZE) - ciaSize - 2) / COPY_ENTRY));
|
|
387
|
+
if (plain.length > maxCopies) throw new WireFailure("invalid-input", `The loader needs ${plain.length} copies; it can do at most ${maxCopies}. Join segments in the unpacker capture (--range) or write the loader by hand.`);
|
|
183
388
|
const copierImage = at;
|
|
184
|
-
const copierSize = COPIER_CODE_SIZE + COPY_ENTRY * plain.length + 2;
|
|
389
|
+
const copierSize = COPIER_CODE_SIZE + (state.vic === undefined ? 0 : VIC_SIZE) + ciaSize + COPY_ENTRY * plain.length + 2;
|
|
185
390
|
const end = copierImage + copierSize - 1;
|
|
186
391
|
if (end > LOAD_LIMIT) throw new WireFailure("invalid-input", `The loader would end at ${hex4(end)}, after ${hex4(LOAD_LIMIT)}: loading it would write the I/O area. The image is too large for one loader file.`);
|
|
187
392
|
|
|
@@ -286,7 +491,11 @@ export function planLoader(input: LoaderInput): LoaderPlan {
|
|
|
286
491
|
" adc #7",
|
|
287
492
|
" tax",
|
|
288
493
|
" jmp .next",
|
|
289
|
-
|
|
494
|
+
".done",
|
|
495
|
+
// The CIA part runs after the raster wait: the wait ends its whole lines earlier, so the entry comes in its line.
|
|
496
|
+
...vicLines(state, Math.floor((cia?.ops.reduce((sum, op) => sum + op.cycles, 0) ?? 0) / LINE_CYCLES)),
|
|
497
|
+
...(cia === undefined ? [] : cia.ops.map((op) => ` ${op.text}`)),
|
|
498
|
+
` lda #${hex2(port0)}`,
|
|
290
499
|
" sta $00",
|
|
291
500
|
` lda #${hex2(port1)}`,
|
|
292
501
|
" sta $01",
|
|
@@ -303,6 +512,12 @@ export function planLoader(input: LoaderInput): LoaderPlan {
|
|
|
303
512
|
".count !word 0",
|
|
304
513
|
".index !byte 0",
|
|
305
514
|
".back !byte 0",
|
|
515
|
+
...(state.vic === undefined
|
|
516
|
+
? []
|
|
517
|
+
: [
|
|
518
|
+
`.vicreg !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(item.register)).join(",")}`,
|
|
519
|
+
`.vicval !byte ${VIC_DISPLAY_REGISTERS.map((item) => hex2(state.vic!.registers[item.register]!)).join(",")}`,
|
|
520
|
+
]),
|
|
306
521
|
".table",
|
|
307
522
|
);
|
|
308
523
|
for (const copy of copies) {
|
|
@@ -320,5 +535,37 @@ export function planLoader(input: LoaderInput): LoaderPlan {
|
|
|
320
535
|
stackByte,
|
|
321
536
|
end,
|
|
322
537
|
text: `${lines.join("\n")}\n`,
|
|
538
|
+
...(cia === undefined ? {} : { cia: cia.plan }),
|
|
323
539
|
};
|
|
324
540
|
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The copier's VIC-II part (D60): with the I/O area visible it sets the
|
|
544
|
+
* display registers and the VIC bank of the entry state, then waits for the
|
|
545
|
+
* entry's raster line, so the program starts in the same phase of the frame.
|
|
546
|
+
*/
|
|
547
|
+
function vicLines(state: EntryState, earlier: number): string[] {
|
|
548
|
+
if (state.vic === undefined) return [];
|
|
549
|
+
const line = (rasterLine(state.vic.registers) - earlier + PAL_LINES) % PAL_LINES;
|
|
550
|
+
return [
|
|
551
|
+
" lda #$35",
|
|
552
|
+
" sta $01",
|
|
553
|
+
` ldx #${hex2(VIC_DISPLAY_REGISTERS.length - 1)}`,
|
|
554
|
+
".vic ldy .vicreg,x",
|
|
555
|
+
" lda .vicval,x",
|
|
556
|
+
" sta $d000,y",
|
|
557
|
+
" dex",
|
|
558
|
+
" bpl .vic",
|
|
559
|
+
" lda $dd00",
|
|
560
|
+
" and #$fc",
|
|
561
|
+
` ora #${hex2(state.vic.bank)}`,
|
|
562
|
+
" sta $dd00",
|
|
563
|
+
`.raster lda $d012`,
|
|
564
|
+
` cmp #${hex2(line & 0xff)}`,
|
|
565
|
+
" bne .raster",
|
|
566
|
+
" lda $d011",
|
|
567
|
+
" and #$80",
|
|
568
|
+
` cmp #${hex2((line >> 1) & 0x80)}`,
|
|
569
|
+
" bne .raster",
|
|
570
|
+
];
|
|
571
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
9
9
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
10
|
-
import
|
|
10
|
+
import { ANALYZERS } from "#src/knowledge/import.ts";
|
|
11
|
+
import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
|
|
11
12
|
import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, type Mode } from "./opcodes.ts";
|
|
12
13
|
|
|
13
14
|
export interface Range {
|
|
@@ -23,7 +24,8 @@ export interface SourceInput {
|
|
|
23
24
|
entry?: number;
|
|
24
25
|
/** Where Ghidra decoded an instruction, with its length. */
|
|
25
26
|
instructions: ReadonlyMap<number, number>;
|
|
26
|
-
|
|
27
|
+
/** Without an origin, a symbol counts as a user or LLM name. */
|
|
28
|
+
symbols: ReadonlyArray<{ address: number; name: string; origin?: Origin }>;
|
|
27
29
|
regions: ReadonlyArray<{ start: number; end: number; type: RegionType; element?: number }>;
|
|
28
30
|
comments: ReadonlyArray<{ address: number; placement: CommentPlacement; text: string }>;
|
|
29
31
|
modules: ReadonlyArray<{ start: number; file: string; title?: string }>;
|
|
@@ -31,13 +33,10 @@ export interface SourceInput {
|
|
|
31
33
|
referenceTargets: readonly number[];
|
|
32
34
|
/** Joined coverage files (D40): an instruction that no run executed is marked (D49). */
|
|
33
35
|
coverage?: Uint8Array;
|
|
34
|
-
/** Platform names (D52): the name of an address that knowledge does not name. */
|
|
35
|
-
platform?:
|
|
36
|
+
/** Platform names (D52): the name of an address that knowledge does not name, and the last address of its entry. */
|
|
37
|
+
platform?: ReadonlyArray<{ address: number; end: number; name: string }>;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/** Files that the generator writes itself; a module cannot have these names. */
|
|
39
|
-
export const GENERATED_FILES = ["main.asm", "loader.asm", "constants.asm"] as const;
|
|
40
|
-
|
|
41
40
|
const CODE_TYPES: readonly RegionType[] = ["code", "jumps", "dual-use"];
|
|
42
41
|
const BYTES_PER_LINE = 8;
|
|
43
42
|
const WORDS_PER_LINE = 4;
|
|
@@ -106,6 +105,24 @@ function operandValue(address: number, opcode: number, byteAt: (address: number)
|
|
|
106
105
|
return low;
|
|
107
106
|
}
|
|
108
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Each entry point inside a data region of the knowledge, with that region.
|
|
110
|
+
* A data region wins over an entry (D44, D48): Ghidra does not decode there,
|
|
111
|
+
* and the source has data there. A code region in the knowledge changes that (F-27).
|
|
112
|
+
*/
|
|
113
|
+
export function entriesInData<R extends { start: number; end: number; type: RegionType }>(entries: readonly number[], regions: readonly R[]): Array<{ entry: number; region: R }> {
|
|
114
|
+
return entries.flatMap((entry) => {
|
|
115
|
+
const region = regions.find((row) => entry >= row.start && entry <= row.end && !CODE_TYPES.includes(row.type));
|
|
116
|
+
return region === undefined ? [] : [{ entry, region }];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Why an image without its entry fails: the image does not hold the program (R2-02). Undefined when the entry is in a segment, or the image has no entry. */
|
|
121
|
+
export function entryOutsideImage(entry: number | undefined, segments: readonly Range[]): string | undefined {
|
|
122
|
+
if (entry === undefined || segments.some((segment) => entry >= segment.start && entry <= segment.end)) return undefined;
|
|
123
|
+
return `The entry ${hex4(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
/** Splits the segments into pieces at module starts and lays out instructions and data. */
|
|
110
127
|
export function layout(input: SourceInput): Layout {
|
|
111
128
|
const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
|
|
@@ -116,7 +133,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
116
133
|
};
|
|
117
134
|
|
|
118
135
|
for (const module of input.modules) {
|
|
119
|
-
if ((
|
|
136
|
+
if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
|
|
120
137
|
}
|
|
121
138
|
const modules = [...input.modules].sort((a, b) => a.start - b.start);
|
|
122
139
|
const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
|
|
@@ -181,13 +198,23 @@ export function layout(input: SourceInput): Layout {
|
|
|
181
198
|
const wanted = new Set<number>();
|
|
182
199
|
const want = (address: number) => inImage(address) && wanted.add(address);
|
|
183
200
|
const outside = new Set<number>();
|
|
184
|
-
const symbols = new Map(input.symbols.map((symbol) => [symbol.address, symbol.name]));
|
|
185
201
|
// A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
|
|
186
202
|
const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
|
|
187
|
-
const platform = new Map(
|
|
203
|
+
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
|
|
204
|
+
// An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins (D61).
|
|
205
|
+
const isAnalyzer = (origin: Origin | undefined) => (ANALYZERS as readonly (Origin | undefined)[]).includes(origin);
|
|
206
|
+
const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
|
|
207
|
+
/** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry (F-20). */
|
|
208
|
+
const insideEntry = new Map<number, number>();
|
|
188
209
|
const use = (address: number) => {
|
|
189
210
|
if (inImage(address)) wanted.add(address);
|
|
190
211
|
else if (symbols.has(address) || platform.has(address)) outside.add(address);
|
|
212
|
+
else {
|
|
213
|
+
const entry = input.platform?.find((row) => address > row.address && address <= row.end && !inImage(row.address) && (symbols.has(row.address) || platform.has(row.address)));
|
|
214
|
+
if (entry === undefined) return;
|
|
215
|
+
outside.add(entry.address);
|
|
216
|
+
insideEntry.set(address, entry.address);
|
|
217
|
+
}
|
|
191
218
|
};
|
|
192
219
|
for (const piece of pieces) {
|
|
193
220
|
for (const item of piece.items) {
|
|
@@ -226,6 +253,8 @@ export function layout(input: SourceInput): Layout {
|
|
|
226
253
|
names.set(address, name);
|
|
227
254
|
generatedLabels++;
|
|
228
255
|
}
|
|
256
|
+
// The high byte of a vector is NMINV+1. A zero-page entry keeps zero-page instructions: its constant has two digits.
|
|
257
|
+
for (const [address, start] of insideEntry) names.set(address, `${names.get(start)!}+${address - start}`);
|
|
229
258
|
return {
|
|
230
259
|
pieces,
|
|
231
260
|
names,
|
|
@@ -490,6 +519,8 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
490
519
|
const out: string[] = [`; ${piece.title}`, `; ${hex4(piece.start)}-${hex4(piece.end)}`, ""];
|
|
491
520
|
/** True while the instructions before have not executed: they form one range. */
|
|
492
521
|
let running = false;
|
|
522
|
+
/** The line of the mark of the open range of instructions that did not execute. */
|
|
523
|
+
let markLine = 0;
|
|
493
524
|
const head = (address: number) => {
|
|
494
525
|
for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
|
|
495
526
|
const label = layout.names.get(address);
|
|
@@ -503,8 +534,12 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
503
534
|
if (running) notExecuted.ranges.at(-1)!.end = item.address + item.length - 1;
|
|
504
535
|
else {
|
|
505
536
|
notExecuted.ranges.push({ start: item.address, end: item.address + item.length - 1 });
|
|
506
|
-
out.
|
|
537
|
+
markLine = out.length;
|
|
538
|
+
out.push("");
|
|
507
539
|
}
|
|
540
|
+
// The mark gives the range, so a reader finds it in the full list (D49).
|
|
541
|
+
const range = notExecuted.ranges.at(-1)!;
|
|
542
|
+
out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
|
|
508
543
|
notExecuted.instructions++;
|
|
509
544
|
}
|
|
510
545
|
running = !executed;
|
|
@@ -31,7 +31,7 @@ node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '
|
|
|
31
31
|
| `--listing analysis/game.lst` | DXA only: write the disassembly listing to this project file. |
|
|
32
32
|
| `--decompile '$0818'` | Ghidra only: decompile the routine at this address. Use it again for more routines, at most 32. |
|
|
33
33
|
| `--flat64k` | The file is 64 KiB of memory from `$0000`, not a PRG. |
|
|
34
|
-
| `--max-listed 20` | List at most this number of labels or
|
|
34
|
+
| `--max-listed 20` | List at most this number of labels, functions or candidates. The default is 50. Use 0 to list none. |
|
|
35
35
|
| `--instructions analysis/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. With `--coverage`, `executed` tells if it ran. |
|
|
36
36
|
| `--space drive8` | The knowledge space of the image: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
|
|
37
37
|
| `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` (action `read` with `out`). Use it again for more runs. The executed code becomes entry points. |
|
|
@@ -63,9 +63,9 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
63
63
|
- `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
|
|
64
64
|
- DXA: `regions` lists the code and data regions. `labels` gives the number of names that DXA found (`count`) and lists the first names (`listed`).
|
|
65
65
|
- Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
|
|
66
|
-
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them.
|
|
66
|
+
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
|
|
67
67
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
68
|
-
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding and opcodes that the program wrote and then
|
|
68
|
+
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
69
69
|
- With `--coverage`, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
|
|
70
70
|
6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
|
|
71
71
|
|
|
@@ -108,7 +108,7 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
|
|
|
108
108
|
- `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
|
|
109
109
|
- `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
|
|
110
110
|
- `operation-failed` with a message about an incomplete result or listing: the analyzer did not give a complete result. The script recorded nothing. Tell the user.
|
|
111
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
111
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
112
112
|
- `notDecompiled`: Ghidra did not decompile these routines. Each entry has the reason. If the reason says that the decompiler did not start, tell the user the remedy in the reason. On macOS, Ghidra has no native decompiler until the user builds it: `./gradlew buildNatives` in the `support/gradle` directory of Ghidra.
|
|
113
113
|
|
|
114
114
|
## Handoffs
|