@henols/c64-re-tools 2.0.0-rc.4 → 2.0.0-rc.5
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 +3 -1
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-basic/SKILL.md +3 -3
- package/skills/c64-emulator/SKILL.md +2 -2
- package/skills/c64-knowledge/SKILL.md +17 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-memory-map/scripts/memmap.ts +1 -1
- package/skills/c64-provenance/SKILL.md +3 -2
- package/skills/c64-provenance/scripts/provenance.ts +4 -2
- package/skills/c64-provenance/scripts/runtime.ts +14 -2
- package/skills/c64-source/SKILL.md +4 -4
- package/skills/c64-source/scripts/generate.ts +57 -14
- package/skills/c64-source/scripts/loader.ts +5 -1
- package/skills/c64-source/scripts/source.ts +3 -1
- package/skills/c64-static-analysis/SKILL.md +2 -2
- package/skills/c64-static-analysis/scripts/analyze.ts +8 -4
- package/skills/c64-unpacker/SKILL.md +5 -5
- package/skills/c64-unpacker/scripts/evidence.ts +29 -22
- package/skills/c64-unpacker/scripts/loads.ts +30 -7
- package/skills/c64-unpacker/scripts/stages.ts +67 -0
- package/skills/c64-unpacker/scripts/unpack.ts +167 -47
- package/src/c64.ts +37 -12
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +9 -7
- package/src/host/main.ts +8 -3
- package/src/host/tools/petcat.ts +5 -1
- package/src/host/tools/status.ts +8 -2
- package/src/host/vice/process.ts +10 -5
- package/src/host/vice/session.ts +26 -13
- package/src/host-client/connect.ts +2 -1
- package/src/host-client/coverage.ts +0 -5
- package/src/host-client/vice-session.ts +37 -4
- package/src/knowledge/history.ts +8 -0
- package/src/knowledge/import.ts +34 -3
- package/src/knowledge/write.ts +2 -1
- package/src/mcp/main.ts +43 -15
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/execution.ts +3 -2
- package/src/mcp/tools/input.ts +6 -4
- package/src/mcp/tools/media.ts +4 -3
- package/src/native/discover.ts +38 -4
- package/src/package.ts +21 -0
- package/src/protocol/tools.ts +7 -3
- package/src/protocol/vice.ts +14 -6
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ The design is in [`docs/redesign/`](docs/redesign/README.md), and
|
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
22
|
-
You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files. ACME, DXA and Ghidra are needed only for the skills that use them: [docs/install-tools.md](docs/install-tools.md) has the steps for each system.
|
|
22
|
+
You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files; use the latest VICE release. `status` and the host name the known flaws of an older VICE. ACME, DXA and Ghidra are needed only for the skills that use them: [docs/install-tools.md](docs/install-tools.md) has the steps for each system.
|
|
23
23
|
|
|
24
24
|
Start the Host Runtime and leave it running:
|
|
25
25
|
|
|
@@ -50,6 +50,8 @@ The agent runs in the container, VICE and the Host Runtime on the host. In `.dev
|
|
|
50
50
|
"runArgs": ["--add-host=host.docker.internal:host-gateway"]
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
Until 2.0.0 is released, the `latest` URL has no Feature. Use the prerelease: `https://github.com/henols/c64-re-tools/releases/download/v2.0.0-rc.4/devcontainer-feature-c64-re-tools.tgz` with `{ "target": "claude", "channel": "next" }`.
|
|
54
|
+
|
|
53
55
|
The Feature installs ACME, dxa, Java 21 and Ghidra, puts the toolkit into the project when the container is created, and updates it at each start. `target` is a harness or a comma list. `channel` is `latest` (default) or `next`. Debian and Ubuntu images only. `runArgs` is for Docker on Linux. Start the Host Runtime on the host as above.
|
|
54
56
|
|
|
55
57
|
### Settings
|
package/distribution/package.ts
CHANGED
|
@@ -2,22 +2,7 @@
|
|
|
2
2
|
// only reads package.json, so a caller that needs the declaration does not
|
|
3
3
|
// build the plugin.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The npm dist-tag that a version is published under: a prerelease
|
|
10
|
-
* (2.0.0-rc.1) goes to "next", a release to "latest".
|
|
11
|
-
*/
|
|
12
|
-
export function channelOf(version: string): "latest" | "next" {
|
|
13
|
-
return version.includes("-") ? "next" : "latest";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** The name, version and dist-tag of this npm package, from its package.json. */
|
|
17
|
-
export const PACKAGE = (() => {
|
|
18
|
-
const metadata = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { name: string; version: string };
|
|
19
|
-
return { name: metadata.name, version: metadata.version, tag: channelOf(metadata.version) };
|
|
20
|
-
})();
|
|
5
|
+
import { PACKAGE } from "../src/package.ts";
|
|
21
6
|
|
|
22
7
|
/**
|
|
23
8
|
* The agent starts the MCP server through npx with the newest version of the
|
package/distribution/plugin.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
13
13
|
|
|
14
14
|
import { definePlugin, defineSkill, type Skill } from "@jalco/ap-sdk";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { PACKAGE } from "../src/package.ts";
|
|
17
|
+
import { MCP_SERVER } from "./package.ts";
|
|
17
18
|
|
|
18
19
|
const root = resolve(import.meta.dirname, "..");
|
|
19
20
|
const SOURCE = join(root, "src");
|
|
@@ -27,9 +28,10 @@ const SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\bnew URL\(\s*)(["'])((?:\.\.?
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* The package.json of an installed skill: ES modules, #src/* resolved inside
|
|
30
|
-
* the skill, and the version of c64-re-tools that the copy comes
|
|
31
|
+
* the skill, and the name and version of c64-re-tools that the copy comes
|
|
32
|
+
* from, which src/package.ts reads.
|
|
31
33
|
*/
|
|
32
|
-
export const SKILL_PACKAGE = `${JSON.stringify({ type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
|
|
34
|
+
export const SKILL_PACKAGE = `${JSON.stringify({ name: PACKAGE.name, type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
|
|
33
35
|
|
|
34
36
|
/** The scripts that a SKILL.md runs, by file name. */
|
|
35
37
|
export function scriptsOf(skillMarkdown: string): string[] {
|
package/package.json
CHANGED
|
@@ -30,9 +30,9 @@ The script prints one JSON object. Addresses are `$` and four hex digits.
|
|
|
30
30
|
1. Run the script on the program.
|
|
31
31
|
2. Read `listing`. Each line starts with its BASIC line number. Keywords show in lower case. Control characters in strings show in braces, for example `{clr}`.
|
|
32
32
|
3. Read `handoffs`. Each handoff has a `kind` (`sys` or `usr`) and the BASIC `line`.
|
|
33
|
-
- A `sys` handoff with an `address`: BASIC calls the machine code at that address.
|
|
34
|
-
- A handoff with `computed: true`: the address comes from a calculation at run time
|
|
35
|
-
4. Compare the address with `loadRange` and `basicEnd`. From `basicEnd` to the end of `loadRange`, the file holds bytes that are not BASIC. Usually this is the machine code that the handoff calls.
|
|
33
|
+
- A `sys` handoff with an `address`: BASIC calls the machine code at that address. The script calculates a SYS argument with numbers, π, `+`, `-`, `*`, `/` and brackets. For example, `SYS π*656` calls `$080c`.
|
|
34
|
+
- A handoff with `computed: true`: the address comes from a calculation at run time, for example with `PEEK`. A `sys` handoff gives its argument in `expression`. Use the c64-emulator skill to find the address.
|
|
35
|
+
4. Compare the address with `loadRange` and `basicEnd`. From `basicEnd` to the end of `loadRange`, the file holds bytes that are not BASIC. Usually this is the machine code that the handoff calls. Some programs have no end marker (two zero bytes) after the last line. The machine code starts directly after the last line. Then `basicEnd` is the first byte of that machine code.
|
|
36
36
|
|
|
37
37
|
The address ranges are for a load to the load address of the file. A load without `,1` puts a BASIC program at `$0801`. Most loaders have `$0801` as their load address.
|
|
38
38
|
|
|
@@ -70,7 +70,7 @@ Step, next and until-return work only in space `c64`. The emulator cannot stop t
|
|
|
70
70
|
|
|
71
71
|
`c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. The emulator has no command that presses one key of the keyboard matrix. For such a program:
|
|
72
72
|
|
|
73
|
-
-
|
|
73
|
+
- VICE 3.10 and older have a known flaw: bits 5-7 of `$DC00` and `$DC01` are always 0. Then `emulatorFlaw` in the result of `c64_keyboard` and `c64_joystick` gives the flaw. With this flaw, a wait for any key that reads all keyboard columns at one time ends without a key. A wait for one specific key, or a compare of the full byte of a joystick port, never ends. Tell the user about the flaw. The next VICE release and the VICE nightly builds do not have it. Joystick port 1 uses the same lines as the keyboard.
|
|
74
74
|
- For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
|
|
75
75
|
- Do not change the program to make it accept a key.
|
|
76
76
|
|
|
@@ -114,7 +114,7 @@ If you cannot get the machine into the necessary state, say which state you coul
|
|
|
114
114
|
|
|
115
115
|
- `machine-state-lost` and the message says that the VICE window closed: the window closed before `c64_window` action `close`. The emulator has no window again. The machine state is the same as at the time when the window opened. The breakpoints and the watchpoints stay. Do not use results from the window time. Read the machine state again, then continue.
|
|
116
116
|
- `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
|
|
117
|
-
- `machine-state-lost` with a different message: the
|
|
117
|
+
- `machine-state-lost` with a different message: the connection to the host runtime was lost, and the machine state is gone. Do not continue with results from before. The next call starts a new machine when the host runtime runs. Load the program again, and set the breakpoints and the watchpoints again. If the next call gives `machine-unavailable` with a message that the host runtime does not run, obey the rule for that message.
|
|
118
118
|
- `machine-unavailable` and the message names a pause or a dialog in the emulator window: this occurs only while the window is open. Do not send more operations. Tell the user to resume the emulator (Pause, Alt+P) or to close the dialog in the VICE window. Then do the operation again. The machine state does not change.
|
|
119
119
|
- `machine-running`: stop the CPU first, then try again.
|
|
120
120
|
- `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. Do not try to repair the installation.
|
|
@@ -76,7 +76,7 @@ Region types:
|
|
|
76
76
|
Read:
|
|
77
77
|
|
|
78
78
|
1. Use `at` for the addresses in the question before other work.
|
|
79
|
-
2. Use `history` when the current knowledge is surprising or disputed.
|
|
79
|
+
2. Use `history` when the current knowledge is surprising or disputed. Each row gives the `reason` and the `evidence` of the revision that wrote it.
|
|
80
80
|
|
|
81
81
|
Write:
|
|
82
82
|
|
|
@@ -97,11 +97,25 @@ For more than a few writes, put them in one JSON file and use `apply`. The file
|
|
|
97
97
|
]
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
The ops
|
|
100
|
+
The ops and their fields:
|
|
101
|
+
|
|
102
|
+
| op | Fields |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `rename` | `address`, `name`, `kind` (optional) |
|
|
105
|
+
| `remove-symbol` | `address` |
|
|
106
|
+
| `classify` | `start`, `end`, `type`, `element` (optional) |
|
|
107
|
+
| `unclassify` | `start`, `end` |
|
|
108
|
+
| `comment` | `address`, `placement` (`line` or `side`), `text` |
|
|
109
|
+
| `uncomment` | `address`, `placement` |
|
|
110
|
+
| `reference`, `unreference` | `from`, `to`, `kind` |
|
|
111
|
+
| `module` | `start`, `file`, `title` (optional) |
|
|
112
|
+
| `unmodule` | `start` |
|
|
113
|
+
|
|
114
|
+
A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
|
|
101
115
|
|
|
102
116
|
## Modules
|
|
103
117
|
|
|
104
|
-
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. Modules have a history, and `revert` restores them.
|
|
118
|
+
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
|
|
105
119
|
|
|
106
120
|
## Removals
|
|
107
121
|
|
|
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
10
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
11
11
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
12
|
-
import { historyAt, historyNamed, revision, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
12
|
+
import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
13
13
|
import {
|
|
14
14
|
at,
|
|
15
15
|
currentRevision,
|
|
@@ -281,7 +281,10 @@ function run(argv: string[]): unknown {
|
|
|
281
281
|
case "spaces":
|
|
282
282
|
return { spaces: listSpaces(db) };
|
|
283
283
|
case "history":
|
|
284
|
-
|
|
284
|
+
// Each row with the reason and the evidence of the revision that wrote it (F-26).
|
|
285
|
+
return {
|
|
286
|
+
history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
287
|
+
};
|
|
285
288
|
case "revisions": {
|
|
286
289
|
const options: { limit?: number; before?: number } = {};
|
|
287
290
|
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
@@ -79,7 +79,7 @@ function run(argv: string[]): unknown {
|
|
|
79
79
|
case "export": {
|
|
80
80
|
if (rest.length !== 0 || values.out === undefined) throw new UsageError("export takes --out <file> and nothing else");
|
|
81
81
|
const names = platformNames();
|
|
82
|
-
const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${hex(item.address)}`.padEnd(24) + `; ${item.meaning}`)];
|
|
82
|
+
const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${hex(item.address, item.address < 0x100 ? 2 : 4)}`.padEnd(24) + `; ${item.meaning}`)];
|
|
83
83
|
const bytes = Buffer.from(`${lines.join("\n")}\n`, "utf8");
|
|
84
84
|
writeProjectFile(values.out, bytes, { what: "--out" });
|
|
85
85
|
return { out: values.out, names: names.length, bytes: bytes.length };
|
|
@@ -48,10 +48,11 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
|
|
|
48
48
|
2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with `--shift`.
|
|
49
49
|
3. Read `differences`. Each one has an address range, the bytes of each release, and `groups`: the releases that have the same bytes there.
|
|
50
50
|
4. To know how the program uses the differences, run the script again with `--run` and the frames that get the program to the part you examine. Read `runtime` of each difference:
|
|
51
|
-
- `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`.
|
|
51
|
+
- `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`. The operand bytes of an executed instruction count as `execute`, for example the value of `LDA #$03`.
|
|
52
52
|
- `changed`: `true` when the bytes at the end of the run are not the bytes of the release.
|
|
53
53
|
- A range that each release writes before it reads is probably runtime state, not a difference of the releases.
|
|
54
54
|
- A range with `none` in each release is not used in this run. Run more frames or other steps before you decide.
|
|
55
|
+
- `run.skipped`: the releases have no differences, so no release ran.
|
|
55
56
|
5. Examine each important difference:
|
|
56
57
|
- Disassemble the bytes of each release with the c64-emulator skill, or analyze them with the c64-static-analysis skill.
|
|
57
58
|
- Find what the change does, for example a trainer, a removed copy protection, or a changed text.
|
|
@@ -62,7 +63,7 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
|
|
|
62
63
|
|
|
63
64
|
Use `separate` when you have only one release. It gives evidence, not a decision:
|
|
64
65
|
|
|
65
|
-
- `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader.
|
|
66
|
+
- `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader. Only an image from c64-unpacker `capture` has stages. For an image from `loads`, `noStages` tells this.
|
|
66
67
|
- `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
|
|
67
68
|
- `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
|
|
68
69
|
- `outside`: RAM outside the image that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write.
|
|
@@ -252,7 +252,9 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
252
252
|
})),
|
|
253
253
|
...(comparison.moreDifferences ? { moreDifferences: true } : {}),
|
|
254
254
|
...(suggestions.length > 0 ? { suggestedShifts: suggestions } : {}),
|
|
255
|
-
...(frames === undefined
|
|
255
|
+
...(frames === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
|
|
256
258
|
};
|
|
257
259
|
}
|
|
258
260
|
|
|
@@ -293,7 +295,7 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
|
|
|
293
295
|
release: path,
|
|
294
296
|
...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
|
|
295
297
|
...(lastStage === undefined
|
|
296
|
-
? {}
|
|
298
|
+
? { noStages: "image.json has no stage log. Only a capture of c64-unpacker writes one. A capture of loads holds one part of a program and has none." }
|
|
297
299
|
: {
|
|
298
300
|
stages: lastStage + 1,
|
|
299
301
|
bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Watchpoints and range breakpoints stop the run at an access; the coverage
|
|
5
5
|
// map then tells which range and which access it was.
|
|
6
6
|
|
|
7
|
-
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
7
|
+
import { COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
|
|
8
8
|
import { runProgram } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
10
|
import { runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
@@ -108,7 +108,12 @@ export async function watchRelease(
|
|
|
108
108
|
}
|
|
109
109
|
await client.memmap({ action: "clear" });
|
|
110
110
|
|
|
111
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* After a stop: the ranges that the coverage shows accessed get their first
|
|
113
|
+
* access, and lose their points. VICE marks only the opcode byte of an
|
|
114
|
+
* executed instruction, so an instruction that starts up to two bytes before
|
|
115
|
+
* a range, or in it, executes its operand bytes too (F-27).
|
|
116
|
+
*/
|
|
112
117
|
const settle = async (): Promise<boolean> => {
|
|
113
118
|
const map = await readCoverage(client);
|
|
114
119
|
let found = false;
|
|
@@ -116,6 +121,13 @@ export async function watchRelease(
|
|
|
116
121
|
const range = ranges[index]!;
|
|
117
122
|
let flags = 0;
|
|
118
123
|
for (let address = range.start; address <= range.end; address++) flags |= map[address]!;
|
|
124
|
+
const from = Math.max(0, range.start - 2);
|
|
125
|
+
if ((flags & EXECUTE) === 0 && map.subarray(from, range.start).some((byte) => (byte & COVERAGE.ramExecute) !== 0)) {
|
|
126
|
+
const code = Buffer.from((await client.memoryRead({ address: from, size: range.start - from, space: "c64", view: "ram" })).data, "hex");
|
|
127
|
+
for (let address = from; address < range.start; address++) {
|
|
128
|
+
if ((map[address]! & COVERAGE.ramExecute) !== 0 && address + instructionLength(code[address - from]!) - 1 >= range.start) flags |= COVERAGE.ramExecute;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
119
131
|
if (accessOf(flags) === "none") continue;
|
|
120
132
|
first[index] = accessOf(flags);
|
|
121
133
|
for (const point of ids) {
|
|
@@ -74,7 +74,7 @@ Do not edit the generated files. The next run writes over them.
|
|
|
74
74
|
|
|
75
75
|
The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the VIC, SID and CIA registers. They stay as the KERNAL set them.
|
|
76
76
|
|
|
77
|
-
The script simulates the copies first. Then it runs `loader.prg` in the emulator to the entry. It compares the memory, $00/$01 and the registers with the entry state. `loader.verified: true` tells that all of them are equal.
|
|
77
|
+
The script simulates the copies first. Then it runs `loader.prg` in the emulator to its jump to the entry. It compares the memory, $00/$01 and the registers with the entry state there. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
|
|
78
78
|
|
|
79
79
|
## Knowledge
|
|
80
80
|
|
|
@@ -98,7 +98,7 @@ One JSON object:
|
|
|
98
98
|
- `unusedModules`: modules that start outside the image.
|
|
99
99
|
- `notExecuted`: with `--coverage`, the count of instructions that no run executed, and their ranges. Put these ranges in the report.
|
|
100
100
|
- `check`: `identical`, and the result for each segment. A different segment gives its first different address.
|
|
101
|
-
- `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`.
|
|
101
|
+
- `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. When the script cannot make the loader, `loader` has `verified: false` and a `reason`. The loader includes the source. Thus a source that does not assemble to the image also stops the loader.
|
|
102
102
|
|
|
103
103
|
The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check and for an error. The exit status is 2 for a wrong argument.
|
|
104
104
|
|
|
@@ -107,8 +107,8 @@ The exit status is 0 when the source is identical and the emulator check of the
|
|
|
107
107
|
- `check.diagnostics`: ACME did not assemble the source. A name in the knowledge can be wrong for ACME. Read the diagnostics and change the name with c64-knowledge.
|
|
108
108
|
- `check.identical: false`: the script has a defect. Give the result to the user.
|
|
109
109
|
- `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
|
|
110
|
-
- `
|
|
111
|
-
- `loader.verified: false`: `differences` gives the bytes and registers that are not equal.
|
|
110
|
+
- `loader.verified: false` with a `reason`: the script did not make the loader. The script still writes and checks the source. When no copy order or no free pages exist, or the loader needs too many copies, the reason gives the ranges. Capture the image again with fewer segments, or write the loader by hand.
|
|
111
|
+
- `loader.verified: false` with `differences`: the loader ran, and `differences` gives the bytes and registers that are not equal.
|
|
112
112
|
- `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 disassembler or emulator script.
|
|
113
113
|
|
|
114
114
|
## Handoffs
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// checks it in the emulator. Exit status: 0 when every check passes, 1 when
|
|
6
6
|
// one fails, 2 for a bad call.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { join, posix } from "node:path";
|
|
9
10
|
import { parseArgs } from "node:util";
|
|
10
11
|
|
|
11
12
|
import { formatC64Address, parseC64Address, prgRange, textToPetscii } from "#src/c64.ts";
|
|
@@ -20,11 +21,11 @@ import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowle
|
|
|
20
21
|
import { assemble } from "#src/native/acme.ts";
|
|
21
22
|
import { analyze } from "#src/native/ghidra/analyze.ts";
|
|
22
23
|
import { localToolContext } from "#src/native/local.ts";
|
|
23
|
-
import { GHIDRA_LIMITS, type Diagnostic } from "#src/native/types.ts";
|
|
24
|
+
import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
|
|
24
25
|
import { platformNames } from "#src/platform.ts";
|
|
25
26
|
import { resolveProjectPath } from "#src/project.ts";
|
|
26
27
|
import { runScript, UsageError } from "#src/script.ts";
|
|
27
|
-
import { planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
28
|
+
import { LOADER_JUMP, planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
28
29
|
import { OPCODES } from "./opcodes.ts";
|
|
29
30
|
import { instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
|
|
30
31
|
|
|
@@ -148,7 +149,7 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/** Assembles files given as text with ACME; the program or the diagnostics. */
|
|
151
|
-
async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; diagnostics: Diagnostic[] }> {
|
|
152
|
+
async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
|
|
152
153
|
const contents = files.map((file) => Buffer.from(file.text, "utf8"));
|
|
153
154
|
const { result, attachments } = await assemble(
|
|
154
155
|
{ files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
|
|
@@ -156,7 +157,7 @@ async function assembleFiles(files: Array<{ name: string; text: string }>, entry
|
|
|
156
157
|
localToolContext(),
|
|
157
158
|
);
|
|
158
159
|
if (!result.assembled) return { diagnostics: result.diagnostics };
|
|
159
|
-
return { program: attachments![0]!, start: result.loadRange!.start, diagnostics: result.diagnostics };
|
|
160
|
+
return { program: attachments![0]!, start: result.loadRange!.start, ...(result.symbols === undefined ? {} : { symbols: result.symbols }), diagnostics: result.diagnostics };
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
/**
|
|
@@ -238,7 +239,13 @@ async function readRam(client: ViceSessionClient, start: number, size: number):
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
/** Runs the loader in the emulator to the entry and compares memory, $00/$01 and the registers with the entry state. */
|
|
241
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Runs the loader in the emulator to its jump to the entry and compares the
|
|
244
|
+
* machine with the entry state there. At the jump, every byte and register is
|
|
245
|
+
* set and no interrupt has run since the PLP: the 6502 takes one only after
|
|
246
|
+
* the JMP, as the original program can take one right at its entry (F-32).
|
|
247
|
+
*/
|
|
248
|
+
async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: Image, state: EntryState, jump: number): Promise<{ verified: boolean; differences: string[] }> {
|
|
242
249
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
243
250
|
try {
|
|
244
251
|
await client.reset({ mode: "hard", run: false });
|
|
@@ -246,9 +253,12 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
246
253
|
await client.keyboard(textToPetscii("RUN\n"));
|
|
247
254
|
const copier = await client.runUntil({ target: { kind: "address", address: plan.copier, space: "c64" }, timeoutFrames: RUN_LIMIT_FRAMES });
|
|
248
255
|
if (!copier.reached) return { verified: false, differences: [`The loader did not get to its copier at ${formatC64Address(plan.copier)}; it stopped at ${formatC64Address(copier.pc)}.`] };
|
|
249
|
-
const
|
|
250
|
-
if (!
|
|
256
|
+
const atJump = await client.runUntil({ target: { kind: "address", address: jump, space: "c64" }, timeoutFrames: LOADER_FRAMES });
|
|
257
|
+
if (!atJump.reached) return { verified: false, differences: [`The loader did not get to its jump to the entry at ${formatC64Address(jump)}; it stopped at ${formatC64Address(atJump.pc)}.`] };
|
|
251
258
|
const memory = await readRam(client, 0x0000, 0x10000);
|
|
259
|
+
if (memory[jump] !== 0x4c || (memory[jump + 1]! | (memory[jump + 2]! << 8)) !== state.pc) {
|
|
260
|
+
return { verified: false, differences: [`The instruction at ${formatC64Address(jump)} is not JMP ${formatC64Address(state.pc)}.`] };
|
|
261
|
+
}
|
|
252
262
|
const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
|
|
253
263
|
const registers = await client.registersGet("c64");
|
|
254
264
|
const differences: string[] = [];
|
|
@@ -272,6 +282,22 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
272
282
|
}
|
|
273
283
|
}
|
|
274
284
|
|
|
285
|
+
/** The first words of every file that the script writes. */
|
|
286
|
+
const GENERATED_MARK = "; Generated by c64-source";
|
|
287
|
+
|
|
288
|
+
/** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg; other files stay. */
|
|
289
|
+
function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
|
|
290
|
+
const directory = resolveProjectPath(out);
|
|
291
|
+
if (!existsSync(directory)) return;
|
|
292
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
293
|
+
if (!entry.isFile()) continue;
|
|
294
|
+
const name = entry.name;
|
|
295
|
+
const path = join(directory, name);
|
|
296
|
+
if (name === "loader.prg") rmSync(path, { force: true });
|
|
297
|
+
else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
275
301
|
async function run(argv: string[]): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
276
302
|
const { values, positionals } = parseArgs({
|
|
277
303
|
args: argv,
|
|
@@ -325,11 +351,21 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
325
351
|
const rendered = render(laid, asBytes, header);
|
|
326
352
|
const files = [...rendered.files];
|
|
327
353
|
let loader: LoaderPlan | undefined;
|
|
354
|
+
// 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;
|
|
328
356
|
if (state !== undefined) {
|
|
329
357
|
const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
|
|
330
|
-
|
|
331
|
-
|
|
358
|
+
try {
|
|
359
|
+
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."] });
|
|
360
|
+
files.push({ name: "loader.asm", text: loader.text });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (!(error instanceof WireFailure)) throw error;
|
|
363
|
+
loaderRefused = error.message;
|
|
364
|
+
}
|
|
332
365
|
}
|
|
366
|
+
// Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again (F-25).
|
|
367
|
+
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
|
+
removeOldFiles(values.out, new Set(files.map((file) => file.name)));
|
|
333
369
|
for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
|
|
334
370
|
|
|
335
371
|
const output: Record<string, unknown> = {
|
|
@@ -354,16 +390,20 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
354
390
|
}),
|
|
355
391
|
...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
|
|
356
392
|
};
|
|
393
|
+
// The loader includes the source, so it is built only from a source that is identical to the image.
|
|
394
|
+
const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
|
|
357
395
|
const built = await assembleFiles(files, "main.asm");
|
|
358
396
|
if (built.program === undefined) {
|
|
359
|
-
return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
|
|
397
|
+
return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
|
|
360
398
|
}
|
|
361
399
|
const checked = check(built.program, built.start!, image);
|
|
362
400
|
output.check = checked;
|
|
363
|
-
if (!checked.identical
|
|
401
|
+
if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
|
|
402
|
+
if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
|
|
403
|
+
if (loader === undefined || state === undefined) return { output, failed: false };
|
|
364
404
|
|
|
365
405
|
const loaderBuilt = await assembleFiles(files, "loader.asm");
|
|
366
|
-
if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
|
|
406
|
+
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 };
|
|
367
407
|
const program = loaderBuilt.program;
|
|
368
408
|
// The simulation: the loader in memory as it loads, then every copy in its order, byte by byte.
|
|
369
409
|
const memory = new Int16Array(0x10000).fill(-1);
|
|
@@ -376,12 +416,15 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
376
416
|
if (loaderBuilt.start !== 0x0801 || loaderBuilt.start + program.length - 3 !== loader.end || wrongPacking !== undefined) {
|
|
377
417
|
throw new WireFailure("operation-failed", `The assembled loader does not have the planned layout${wrongPacking === undefined ? "" : ` (${wrongPacking.what})`}. This is a defect of the c64-source script.`);
|
|
378
418
|
}
|
|
419
|
+
const jumpSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === LOADER_JUMP);
|
|
420
|
+
if (jumpSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${LOADER_JUMP} label. This is a defect of the c64-source script.`);
|
|
421
|
+
const jump = jumpSymbol.kind === "address" ? jumpSymbol.address : jumpSymbol.value;
|
|
379
422
|
const wrong = simulate(memory, loader.copies, expected);
|
|
380
423
|
if (wrong !== undefined) throw new WireFailure("operation-failed", `The simulated loader writes a wrong byte at ${formatC64Address(wrong)}. This is a defect of the c64-source script.`);
|
|
381
424
|
const loaderPath = `${values.out}/loader.prg`;
|
|
382
425
|
writeProjectFile(loaderPath, program, { what: "--out" });
|
|
383
426
|
(output.files as string[]).push("loader.prg");
|
|
384
|
-
const ran = await runLoader(loaderPath, program, loader, image, state);
|
|
427
|
+
const ran = await runLoader(loaderPath, program, loader, image, state, jump);
|
|
385
428
|
output.loader = {
|
|
386
429
|
program: loaderPath,
|
|
387
430
|
copies: loader.copies.map((copy) => ({ what: copy.what, to: `${formatC64Address(copy.target.start)}-${formatC64Address(copy.target.end)}` })),
|
|
@@ -88,6 +88,9 @@ export interface LoaderPlan {
|
|
|
88
88
|
text: string;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** The label of the loader's jump to the entry, in ACME's symbol list. */
|
|
92
|
+
export const LOADER_JUMP = "c64_source_loader_jump";
|
|
93
|
+
|
|
91
94
|
const LOAD = 0x0801;
|
|
92
95
|
const STUB = 0x080d;
|
|
93
96
|
/** sei, ldx, the 2-page move loop and jmp. */
|
|
@@ -295,7 +298,8 @@ export function planLoader(input: LoaderInput): LoaderPlan {
|
|
|
295
298
|
` ldx #${hex2(state.x)}`,
|
|
296
299
|
` ldy #${hex2(state.y)}`,
|
|
297
300
|
" plp",
|
|
298
|
-
|
|
301
|
+
// A global label: the check of the loader stops here, before an interrupt can run (F-32).
|
|
302
|
+
`${LOADER_JUMP} jmp ${hex4(input.entry)}`,
|
|
299
303
|
".count !word 0",
|
|
300
304
|
".index !byte 0",
|
|
301
305
|
".back !byte 0",
|
|
@@ -93,6 +93,8 @@ export interface Layout {
|
|
|
93
93
|
|
|
94
94
|
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
95
95
|
const hex4 = (value: number) => formatC64Address(value);
|
|
96
|
+
/** ACME reads leading zeros as a 16-bit value, so a zero-page name gets two digits. */
|
|
97
|
+
const constantValue = (address: number) => (address < 0x100 ? hex2(address) : hex4(address));
|
|
96
98
|
const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
|
|
97
99
|
|
|
98
100
|
function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
|
|
@@ -545,7 +547,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
545
547
|
result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
|
|
546
548
|
}
|
|
547
549
|
|
|
548
|
-
const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${
|
|
550
|
+
const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${constantValue(address)}`)];
|
|
549
551
|
result.files.push({ name: "constants.asm", text: `${constants.join("\n")}\n` });
|
|
550
552
|
const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"'];
|
|
551
553
|
for (const piece of layout.pieces) {
|
|
@@ -66,7 +66,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
66
66
|
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them.
|
|
67
67
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
68
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 executed. The script does not write those. Look at them and decide.
|
|
69
|
-
- With `--coverage`, `coverageConflicts` lists the analyzer data regions where the coverage shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
|
|
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
|
|
|
72
72
|
The script records only structure: generated names, code and data regions, and references from Ghidra. The listing and the decompiled code are only for the current task. The script does not record them in the knowledge.
|
|
@@ -78,7 +78,7 @@ Automatic write:
|
|
|
78
78
|
- names that DXA generated (labels) and routine entry points with names that Ghidra generated
|
|
79
79
|
- references from Ghidra: call, jump, read and write.
|
|
80
80
|
|
|
81
|
-
The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other.
|
|
81
|
+
The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other, with one exception. With `--coverage`, a code finding replaces a DXA or Ghidra data region or data name where a run executed an instruction start. The revision keeps the coverage files as evidence. Thus run DXA first, then Ghidra with all coverage files.
|
|
82
82
|
|
|
83
83
|
A `heuristic` region replaces the part of a DXA or Ghidra region where it lies, because an idiom is specific evidence about those bytes. DXA and Ghidra never replace a `heuristic` region. If they find code there, the result shows a conflict. A heuristic table can be too long or too short, because the script cannot know its length. Correct it with the c64-knowledge skill. Your region replaces it, and a removal holds.
|
|
84
84
|
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// current knowledge, and imports the structural findings into
|
|
4
4
|
// .c64-re-tools/knowledge.db.
|
|
5
5
|
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { dirname } from "node:path";
|
|
8
9
|
import { parseArgs } from "node:util";
|
|
9
10
|
|
|
10
|
-
import { formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
|
|
11
|
+
import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
|
|
11
12
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
13
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
13
14
|
import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
|
|
@@ -93,7 +94,7 @@ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<stri
|
|
|
93
94
|
* changed since the seeds were read. A refused import creates no knowledge.db.
|
|
94
95
|
* The revision records the image hash and the analyzer version.
|
|
95
96
|
*/
|
|
96
|
-
function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion">) {
|
|
97
|
+
function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
|
|
97
98
|
let imported: ImportResult;
|
|
98
99
|
try {
|
|
99
100
|
imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
|
|
@@ -159,6 +160,9 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
159
160
|
const bytes = readProjectFile(image).bytes;
|
|
160
161
|
const coverage = readCoverageFiles(values.coverage ?? []);
|
|
161
162
|
const byteAt = imageReader(bytes, imageKind);
|
|
163
|
+
// With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58), and the files are the evidence.
|
|
164
|
+
const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
|
|
165
|
+
const coverageEvidence = coverage === undefined ? {} : { evidence: (values.coverage ?? []).map((path) => ({ kind: "coverage" as const, ref: path, sha256: createHash("sha256").update(readProjectFile(path).bytes).digest("hex") })) };
|
|
162
166
|
|
|
163
167
|
let loaded: { start: number; end: number };
|
|
164
168
|
try {
|
|
@@ -212,7 +216,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
212
216
|
const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
|
|
213
217
|
const listing = attachments[0]!.toString("utf8");
|
|
214
218
|
// The listing is written only after the import: a refused import leaves no new file.
|
|
215
|
-
const imported = record({ ...dxaFindings(result), space }, revision, `DXA analysis of ${image}`, runProvenance(bytes, result.toolVersion));
|
|
219
|
+
const imported = record({ ...dxaFindings(result), space, ...executed }, revision, `DXA analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
|
|
216
220
|
if (listingPath !== undefined) {
|
|
217
221
|
try {
|
|
218
222
|
mkdirSync(dirname(listingPath), { recursive: true });
|
|
@@ -237,7 +241,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
|
|
240
|
-
const imported = record({ ...ghidraFindings(result), space }, revision, `Ghidra analysis of ${image}`, runProvenance(bytes, result.toolVersion));
|
|
244
|
+
const imported = record({ ...ghidraFindings(result), space, ...executed }, revision, `Ghidra analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
|
|
241
245
|
const found = idiomsResult();
|
|
242
246
|
let instructions: { out: string; count: number } | undefined;
|
|
243
247
|
if (values.instructions !== undefined) {
|
|
@@ -35,13 +35,13 @@ node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out
|
|
|
35
35
|
| `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
|
|
36
36
|
| `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
|
|
37
37
|
| `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
|
|
38
|
-
| `--entry '$c000'` | `trace`
|
|
38
|
+
| `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
|
|
39
39
|
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
|
|
40
40
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
41
41
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
42
|
-
| `--min-stage 64` | `stages`
|
|
42
|
+
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
43
43
|
|
|
44
|
-
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object.
|
|
44
|
+
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object. The script refuses an option that the command does not use.
|
|
45
45
|
|
|
46
46
|
A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of the c64-testing skill. Addresses are `$` and four hex digits. For example, wait 50 frames and press the space bar:
|
|
47
47
|
|
|
@@ -53,7 +53,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
|
|
|
53
53
|
|
|
54
54
|
## Workflow
|
|
55
55
|
|
|
56
|
-
1. Run `inspect`. Read `basicStart` and `packing
|
|
56
|
+
1. Run `inspect`. Read `basicStart` and `packing`. `basicStart` gives the first SYS line and its address. When the SYS argument needs values at run time, `sys` is null and `expression` gives the argument. Then `trace`, `stages` and `capture` stop at the first instruction after the BASIC text:
|
|
57
57
|
- `likely`: most of the bytes look compressed. Packed data has this property.
|
|
58
58
|
- `unlikely`: the bytes look like code and data. A simple cruncher can also give this result.
|
|
59
59
|
- `unclear`: the file is short, or the blocks do not give clear evidence. Or the code at the SYS address decodes or moves bytes outside the program.
|
|
@@ -64,7 +64,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
|
|
|
64
64
|
- `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
|
|
65
65
|
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
66
66
|
2. Run `trace`. It gives the evidence from the run. Read `packing`:
|
|
67
|
-
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the code that runs
|
|
67
|
+
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is usually the entry point of the unpacked program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
|
|
68
68
|
- `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
|
|
69
69
|
- `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
|
|
70
70
|
3. Find the address where the unpacked program starts:
|