@henols/c64-re-tools 1.0.0 → 2.0.0-rc.10
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/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-unpacker
|
|
3
|
+
description: Use this skill to find out if a packer or a cruncher compressed a C64 program, and to get the unpacked program for analysis. It shows evidence of packing without a guess of the packer name. It runs the program in an emulator until the unpacked code starts, then writes the memory into the project. It also captures each part of a multi-load program and the drive RAM. Do not use it to analyze the unpacked code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-unpacker
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Find out if a packer compressed a C64 program. Get the unpacked program for analysis.
|
|
11
|
+
|
|
12
|
+
This skill does not change project knowledge.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- The program: a PRG file, relative to the project directory.
|
|
17
|
+
- For a capture: the address where the unpacked program starts, and the directory for the result.
|
|
18
|
+
|
|
19
|
+
## Tools / execution path
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
node <skill>/scripts/unpack.ts inspect original/game.prg
|
|
23
|
+
node <skill>/scripts/unpack.ts trace original/game.prg
|
|
24
|
+
node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out unpacked
|
|
25
|
+
node <skill>/scripts/unpack.ts stages original/game.prg --steps input.json
|
|
26
|
+
node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out parts --drive
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages` and `capture`, a `frames` step and a `runUntil` step with only `at` count as run time. `stages` refuses a `runUntil` step with `raster` or `memory`.
|
|
30
|
+
|
|
31
|
+
| Option | Meaning |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `--until '$c000'` | The address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
|
|
34
|
+
| `--out unpacked/game.prg` | Where to write the result, relative to the project directory. |
|
|
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
|
+
| `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
|
|
37
|
+
| `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
|
|
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
|
+
| `--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. The scripts start the program as a c64-testing scenario does. Thus the same steps in a scenario get the program to the entry at the same time. |
|
|
40
|
+
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
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` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
43
|
+
|
|
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
|
+
|
|
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
|
+
|
|
48
|
+
```json
|
|
49
|
+
[{ "frames": 50 }, { "type": " " }]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A `type` step puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. A step cannot press one specific key of the keyboard matrix. A wait for any key that reads all keyboard columns can end without a key.
|
|
53
|
+
|
|
54
|
+
## Workflow
|
|
55
|
+
|
|
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
|
+
- `likely`: most of the bytes look compressed. Packed data has this property.
|
|
58
|
+
- `unlikely`: the bytes look like code and data. A simple cruncher can also give this result.
|
|
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.
|
|
60
|
+
|
|
61
|
+
Also read the evidence that the script finds in the file. The script never names a packer from it:
|
|
62
|
+
- `basicText`: the BASIC lines as text. A packer or a crack often puts its name there.
|
|
63
|
+
- `strings`: readable text near the start of the code.
|
|
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
|
+
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
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 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 code that runs after the unpacking. It is not always the entry point of the program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
|
|
68
|
+
- `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
|
|
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
|
+
3. Find the address where the unpacked program starts:
|
|
71
|
+
- Run `stages`. The `entry` of the last stage is usually the start of the program. A routine that a stage writes and calls is part of that stage when it returns to the stage.
|
|
72
|
+
- Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
|
|
73
|
+
- Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
|
|
74
|
+
- Disassemble the code at the address with the c64-emulator skill. Make sure that it is the start of the program. The start of a program often sets the stack pointer, the interrupts and the vectors.
|
|
75
|
+
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
76
|
+
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
77
|
+
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
78
|
+
- One PRG for each segment.
|
|
79
|
+
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
80
|
+
5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it.
|
|
81
|
+
6. Give `image.bin` to the c64-static-analysis skill with its `--flat64k` option, or give it the segment PRGs.
|
|
82
|
+
7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
|
|
83
|
+
|
|
84
|
+
## Multi-load programs
|
|
85
|
+
|
|
86
|
+
A multi-load program loads more parts from the disk while it runs, for example one part for each level. Use `loads` to capture each part. The program can be a PRG, or a disk, tape or cartridge image that VICE starts.
|
|
87
|
+
|
|
88
|
+
1. Write a steps file. Put a `capture` step after each point where a part is in memory, for example:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
[
|
|
92
|
+
{ "runUntil": { "at": "$080d" }, "timeoutFrames": 6000 },
|
|
93
|
+
{ "capture": "main" },
|
|
94
|
+
{ "type": " " },
|
|
95
|
+
{ "runUntil": { "at": "$c000" }, "timeoutFrames": 6000 },
|
|
96
|
+
{ "capture": "level1" }
|
|
97
|
+
]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
2. Run `loads` with the steps file and a directory in `--out`. Add `--drive` when the program has its own drive code, for example a fast loader.
|
|
101
|
+
3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
|
|
102
|
+
4. With `--drive`, `drive8.prg` is the drive RAM at `$0000`-`$07ff`. Analyze it with c64-static-analysis and `--space drive8`, and record its knowledge in space `drive8`.
|
|
103
|
+
|
|
104
|
+
VICE records no memory access map for the drive CPU. Thus no tool gives coverage of drive code.
|
|
105
|
+
|
|
106
|
+
## Knowledge
|
|
107
|
+
|
|
108
|
+
Automatic write:
|
|
109
|
+
- none.
|
|
110
|
+
|
|
111
|
+
Do not record the unpacker in the project knowledge, unless the user asks to examine the unpacker.
|
|
112
|
+
|
|
113
|
+
## Result
|
|
114
|
+
|
|
115
|
+
The packing evidence, the stages, or the unpacked program with its image, segments and entry state in a directory of the project.
|
|
116
|
+
|
|
117
|
+
For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
|
|
118
|
+
|
|
119
|
+
The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false`, for `loaded: false` and for an error. The exit status is 2 for a wrong argument.
|
|
120
|
+
|
|
121
|
+
## Failure and conflicts
|
|
122
|
+
|
|
123
|
+
- `traced: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
|
|
124
|
+
- `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a key.
|
|
125
|
+
- A step did not reach its target: the reason names the step. Change the step or give it more frames.
|
|
126
|
+
- `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
|
|
127
|
+
- Do not name a packer from weak evidence. The script never names one.
|
|
128
|
+
- Some programs unpack in more than one stage. Run `stages` to find each stage. Capture at the entry of the last stage. Make sure that it is the application and not a second unpacker.
|
|
129
|
+
- `not-found` or `invalid-input`: a path or an argument is wrong. Read the message.
|
|
130
|
+
- `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.
|
|
131
|
+
|
|
132
|
+
## Handoffs
|
|
133
|
+
|
|
134
|
+
- Unpacked machine code → c64-static-analysis
|
|
135
|
+
- Find the start address at run time → c64-emulator
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Local evidence of packing for the c64-unpacker script: where BASIC hands
|
|
2
|
+
// over and how compressed the bytes look. It never names a packer.
|
|
3
|
+
|
|
4
|
+
import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized } from "#src/c64.ts";
|
|
5
|
+
|
|
6
|
+
export class PackingError extends Error {}
|
|
7
|
+
|
|
8
|
+
/** A 256-byte block above this many bits per byte looks compressed. */
|
|
9
|
+
const HIGH_ENTROPY = 6.9;
|
|
10
|
+
const BLOCK = 256;
|
|
11
|
+
|
|
12
|
+
function entropy(bytes: Uint8Array): number {
|
|
13
|
+
const counts = new Array<number>(256).fill(0);
|
|
14
|
+
for (const byte of bytes) counts[byte]!++;
|
|
15
|
+
let bits = 0;
|
|
16
|
+
for (const count of counts) {
|
|
17
|
+
if (count === 0) continue;
|
|
18
|
+
const p = count / bytes.length;
|
|
19
|
+
bits -= p * Math.log2(p);
|
|
20
|
+
}
|
|
21
|
+
return bits;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A BASIC program at $0801, which RUN starts. */
|
|
25
|
+
export interface BasicStart {
|
|
26
|
+
/** The first address after the BASIC text. */
|
|
27
|
+
basicEnd: number;
|
|
28
|
+
/** The first SYS: its line, and its address when the argument is a constant. */
|
|
29
|
+
sys?: { line: number; address?: number; expression?: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The BASIC program of a PRG at $0801 and its first SYS, or undefined when the file is no BASIC program there. */
|
|
33
|
+
export function basicStart(program: Uint8Array): BasicStart | undefined {
|
|
34
|
+
const tokenized = parseTokenized(program);
|
|
35
|
+
if ("reason" in tokenized || tokenized.loadAddress !== 0x0801 || tokenized.lines.length === 0) return undefined;
|
|
36
|
+
const sys = findHandoffs(tokenized.lines).find((handoff) => handoff.kind === "sys");
|
|
37
|
+
if (sys === undefined) return { basicEnd: tokenized.basicEnd };
|
|
38
|
+
return { basicEnd: tokenized.basicEnd, sys: "address" in sys ? { line: sys.line, address: sys.address } : { line: sys.line, expression: "expression" in sys ? sys.expression : "" } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The local packing evidence of one PRG. */
|
|
42
|
+
export interface PackingEvidence {
|
|
43
|
+
loadRange: { start: string; end: string };
|
|
44
|
+
bytes: number;
|
|
45
|
+
/** The first SYS line and its address (null when the argument needs run-time values, with the argument), or null when the program has no SYS. */
|
|
46
|
+
basicStart: { line: number; sys: string | null; expression?: string } | null;
|
|
47
|
+
entropy: { blocks: number; highBlocks: number; median: number | null };
|
|
48
|
+
/** The BASIC lines as text: a packer or a crack often puts its banner there (D43). */
|
|
49
|
+
basicText: string[];
|
|
50
|
+
/** Runs of readable PETSCII near the start of the code, raw. */
|
|
51
|
+
strings: string[];
|
|
52
|
+
/** What the code at the SYS address does in its first bytes. */
|
|
53
|
+
start: { at: string; jumpsOutside?: string; writesOutside?: string } | null;
|
|
54
|
+
/** EOR loops that store with an index near the start: decryption or decoding. */
|
|
55
|
+
decodeLoops: string[];
|
|
56
|
+
packing: "likely" | "unlikely" | "unclear";
|
|
57
|
+
evidence: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How far the start-code signals look from the SYS address. */
|
|
61
|
+
const START_BYTES = 256;
|
|
62
|
+
const MIN_STRING = 8;
|
|
63
|
+
const MAX_STRINGS = 10;
|
|
64
|
+
const EOR_OPCODES = new Set([0x41, 0x45, 0x49, 0x4d, 0x51, 0x55, 0x59, 0x5d]);
|
|
65
|
+
const INDEXED_STORES = new Set([0x81, 0x91, 0x95, 0x99, 0x9d]);
|
|
66
|
+
const BRANCHES = new Set([0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
|
|
67
|
+
|
|
68
|
+
/** Readable PETSCII runs of at least MIN_STRING characters, the first MAX_STRINGS of them. */
|
|
69
|
+
function readableStrings(bytes: Uint8Array): string[] {
|
|
70
|
+
const found: string[] = [];
|
|
71
|
+
let run = "";
|
|
72
|
+
for (const byte of [...bytes, 0]) {
|
|
73
|
+
if (byte >= 0x20 && byte <= 0x5d) run += String.fromCharCode(byte);
|
|
74
|
+
else {
|
|
75
|
+
if (run.trim().length >= MIN_STRING && found.length < MAX_STRINGS) found.push(run.trim().slice(0, 40));
|
|
76
|
+
run = "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The start-code signals: the first JMP to an address outside the loaded
|
|
84
|
+
* range, the first indexed store outside it, and EOR loops that store with
|
|
85
|
+
* an index and branch back.
|
|
86
|
+
*/
|
|
87
|
+
function startSignals(program: Uint8Array, load: number, end: number, sys: number): { start: PackingEvidence["start"]; decodeLoops: string[] } {
|
|
88
|
+
if (sys < load || sys > end) return { start: null, decodeLoops: [] };
|
|
89
|
+
const byteAt = (address: number) => (address >= load && address <= end ? program[address - load + 2]! : undefined);
|
|
90
|
+
const outside = (address: number) => address < load || address > end;
|
|
91
|
+
const instructions: Array<{ at: number; opcode: number; operand: number; length: number }> = [];
|
|
92
|
+
for (let at = sys; at <= Math.min(end, sys + START_BYTES - 1); ) {
|
|
93
|
+
const opcode = byteAt(at)!;
|
|
94
|
+
const length = instructionLength(opcode);
|
|
95
|
+
const operand = length === 3 ? (byteAt(at + 1) ?? 0) | ((byteAt(at + 2) ?? 0) << 8) : length === 2 ? (byteAt(at + 1) ?? 0) : 0;
|
|
96
|
+
instructions.push({ at, opcode, operand, length });
|
|
97
|
+
at += length;
|
|
98
|
+
}
|
|
99
|
+
const jump = instructions.find((instruction) => instruction.opcode === 0x4c && outside(instruction.operand));
|
|
100
|
+
const store = instructions.find((instruction) => (instruction.opcode === 0x9d || instruction.opcode === 0x99) && outside(instruction.operand));
|
|
101
|
+
const decodeLoops = instructions
|
|
102
|
+
.filter((instruction, index) => {
|
|
103
|
+
if (!EOR_OPCODES.has(instruction.opcode)) return false;
|
|
104
|
+
const after = instructions.slice(index + 1, index + 9);
|
|
105
|
+
return after.some((next) => INDEXED_STORES.has(next.opcode)) && after.some((next) => BRANCHES.has(next.opcode) && next.operand >= 0x80);
|
|
106
|
+
})
|
|
107
|
+
.map((instruction) => formatC64Address(instruction.at));
|
|
108
|
+
return {
|
|
109
|
+
start: { at: formatC64Address(sys), ...(jump === undefined ? {} : { jumpsOutside: formatC64Address(jump.operand) }), ...(store === undefined ? {} : { writesOutside: formatC64Address(store.operand) }) },
|
|
110
|
+
decodeLoops,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function inspect(program: Uint8Array): PackingEvidence {
|
|
115
|
+
if (program.length < 3) throw new PackingError("the file is too short for a PRG");
|
|
116
|
+
const load = program[0]! | (program[1]! << 8);
|
|
117
|
+
const end = load + program.length - 3;
|
|
118
|
+
const basic = basicStart(program);
|
|
119
|
+
const stub = basic?.sys;
|
|
120
|
+
// Without BASIC, all bytes count; with it, only the bytes after the BASIC text.
|
|
121
|
+
const from = basic === undefined ? 2 : Math.min(program.length, basic.basicEnd - load + 2);
|
|
122
|
+
const body = program.subarray(from);
|
|
123
|
+
const blocks: number[] = [];
|
|
124
|
+
for (let at = 0; at + BLOCK <= body.length; at += BLOCK) blocks.push(entropy(body.subarray(at, at + BLOCK)));
|
|
125
|
+
const high = blocks.filter((value) => value > HIGH_ENTROPY).length;
|
|
126
|
+
const share = blocks.length === 0 ? 0 : high / blocks.length;
|
|
127
|
+
const byEntropy = blocks.length < 2 ? "unclear" : share >= 0.75 ? "likely" : share <= 0.1 ? "unlikely" : "unclear";
|
|
128
|
+
const tokenized = parseTokenized(program);
|
|
129
|
+
const basicText = "lines" in tokenized && load === 0x0801 ? tokenized.lines.map(basicLineText) : [];
|
|
130
|
+
const signals = stub?.address === undefined ? { start: null, decodeLoops: [] } : startSignals(program, load, end, stub.address);
|
|
131
|
+
const startMoves = signals.start?.jumpsOutside !== undefined || signals.start?.writesOutside !== undefined || signals.decodeLoops.length > 0;
|
|
132
|
+
// Code that decodes or moves itself is a reason to run it, also when the bytes look plain.
|
|
133
|
+
const packing = byEntropy === "unlikely" && startMoves ? "unclear" : byEntropy;
|
|
134
|
+
const sorted = [...blocks].sort((a, b) => a - b);
|
|
135
|
+
return {
|
|
136
|
+
loadRange: { start: formatC64Address(load), end: formatC64Address(Math.min(end, 0xffff)) },
|
|
137
|
+
bytes: program.length - 2,
|
|
138
|
+
basicStart: stub === undefined ? null : stub.address === undefined ? { line: stub.line, sys: null, expression: stub.expression! } : { line: stub.line, sys: formatC64Address(stub.address) },
|
|
139
|
+
entropy: {
|
|
140
|
+
blocks: blocks.length,
|
|
141
|
+
highBlocks: high,
|
|
142
|
+
median: sorted.length === 0 ? null : Number(sorted[sorted.length >> 1]!.toFixed(2)),
|
|
143
|
+
},
|
|
144
|
+
basicText,
|
|
145
|
+
strings: readableStrings(body.subarray(0, 1024)),
|
|
146
|
+
start: signals.start,
|
|
147
|
+
decodeLoops: signals.decodeLoops,
|
|
148
|
+
packing,
|
|
149
|
+
evidence:
|
|
150
|
+
packing === "likely"
|
|
151
|
+
? `${high} of ${blocks.length} blocks of 256 bytes look compressed (above ${HIGH_ENTROPY} bits per byte); code and data are usually below.`
|
|
152
|
+
: packing === "unlikely"
|
|
153
|
+
? `Only ${high} of ${blocks.length} blocks look compressed; the bytes look like code and data. A simple cruncher can look the same: run trace to see if the program writes code and then executes it.`
|
|
154
|
+
: startMoves && byEntropy === "unlikely"
|
|
155
|
+
? "The bytes look like code and data, but the code at the SYS address decodes or moves bytes outside the program. Run trace or stages to see what it writes and runs."
|
|
156
|
+
: "The entropy does not show clearly if the program is packed. Run it and look at what it writes to memory.",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One range of the emulator's memory map: what the program did with these addresses. */
|
|
161
|
+
export interface AccessRange {
|
|
162
|
+
start: number;
|
|
163
|
+
end: number;
|
|
164
|
+
read: boolean;
|
|
165
|
+
write: boolean;
|
|
166
|
+
execute: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reads the memory map of $0000-$ffff in address order. One `read` gives a
|
|
171
|
+
* limited number of ranges, and `next` when more follow: the next read starts there.
|
|
172
|
+
*/
|
|
173
|
+
export async function readMemoryMap(read: (start: number) => Promise<{ ranges: AccessRange[]; next?: number }>): Promise<AccessRange[]> {
|
|
174
|
+
const ranges: AccessRange[] = [];
|
|
175
|
+
for (let start: number | undefined = 0; start !== undefined; ) {
|
|
176
|
+
const found: { ranges: AccessRange[]; next?: number } = await read(start);
|
|
177
|
+
ranges.push(...found.ranges);
|
|
178
|
+
if (found.next !== undefined && found.next <= start) throw new Error(`the memory map did not move on from ${start}`);
|
|
179
|
+
start = found.next;
|
|
180
|
+
}
|
|
181
|
+
return ranges;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** ROM areas: an address here can be written (RAM below) and executed (ROM) without one being the other. */
|
|
185
|
+
export const underRom = (address: number) => (address >= 0xa000 && address <= 0xbfff) || address >= 0xe000;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The memory that the program wrote and then executed, in pieces of code.
|
|
189
|
+
* `bytes` and `running` count only RAM outside the ROM areas: a write under
|
|
190
|
+
* a ROM and code that runs in that ROM are not one piece of code.
|
|
191
|
+
*/
|
|
192
|
+
export function writtenThenExecuted(
|
|
193
|
+
ranges: AccessRange[],
|
|
194
|
+
pc: number,
|
|
195
|
+
): { written: Array<{ start: number; end: number }>; counted: Array<{ start: number; end: number }>; bytes: number; running?: { start: number; end: number } } {
|
|
196
|
+
// VICE marks only opcode bytes as executed; a gap of at most two operand bytes stays inside one piece of code.
|
|
197
|
+
const written: Array<{ start: number; end: number }> = [];
|
|
198
|
+
for (const range of ranges.filter((candidate) => candidate.write && candidate.execute)) {
|
|
199
|
+
const last = written.at(-1);
|
|
200
|
+
if (last !== undefined && range.start - last.end <= 3 && underRom(range.start) === underRom(last.start)) last.end = range.end;
|
|
201
|
+
else written.push({ start: range.start, end: range.end });
|
|
202
|
+
}
|
|
203
|
+
const counted = written.filter((range) => !underRom(range.start));
|
|
204
|
+
const bytes = counted.reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
205
|
+
const running = counted.find((range) => pc >= range.start && pc <= range.end);
|
|
206
|
+
return { written, counted, bytes, ...(running === undefined ? {} : { running }) };
|
|
207
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The loads command of c64-unpacker (D53): one capture for each part of a
|
|
2
|
+
// multi-load program. The steps file has action steps and capture steps; a
|
|
3
|
+
// capture writes what the C64 side changed since the previous capture as an
|
|
4
|
+
// image with its entry state, and with --drive the drive RAM as a PRG for
|
|
5
|
+
// knowledge space drive8. No tool guesses where a load ends: the steps decide.
|
|
6
|
+
|
|
7
|
+
import { COVERAGE, formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
8
|
+
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
|
+
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
|
+
import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
11
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
13
|
+
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
14
|
+
import { UsageError } from "#src/script.ts";
|
|
15
|
+
import { entryState, json, machineState, readRam, vectorsOf } from "./machine.ts";
|
|
16
|
+
|
|
17
|
+
export type LoadStep = ActionStep | { capture: string };
|
|
18
|
+
|
|
19
|
+
/** A capture name: letters, digits, dots, underscores or hyphens. */
|
|
20
|
+
const NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
21
|
+
/** The drive RAM of a 1541. */
|
|
22
|
+
const DRIVE_RAM = 0x0800;
|
|
23
|
+
/** Pages 0-3 belong to the entry state, not to the segments. */
|
|
24
|
+
const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
25
|
+
/** The KERNAL LOAD entry in the jump table. */
|
|
26
|
+
const KERNAL_LOAD = 0xffd5;
|
|
27
|
+
/** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
|
|
28
|
+
const KERNAL_KEY_WAIT = { start: 0xe5cd, end: 0xe5d5 };
|
|
29
|
+
|
|
30
|
+
/** Reads and checks the steps file of loads: action steps with $ addresses, and capture steps with new names. */
|
|
31
|
+
export function readLoadSteps(path: string): LoadStep[] {
|
|
32
|
+
let value: unknown;
|
|
33
|
+
try {
|
|
34
|
+
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof WireFailure) throw error;
|
|
37
|
+
throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
|
|
38
|
+
}
|
|
39
|
+
const fail = (message: string): never => {
|
|
40
|
+
throw new UsageError(`--steps ${path}: ${message}`);
|
|
41
|
+
};
|
|
42
|
+
if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
|
|
43
|
+
const names = new Set<string>();
|
|
44
|
+
value.forEach((step: unknown, index) => {
|
|
45
|
+
const at = `step ${index + 1}`;
|
|
46
|
+
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
47
|
+
if ("capture" in step) {
|
|
48
|
+
if (Object.keys(step).length !== 1) fail(`${at}: a capture step has only capture`);
|
|
49
|
+
if (typeof step.capture !== "string" || !NAME.test(step.capture)) fail(`${at}: capture needs a name of 1 to 64 letters, digits, dots, underscores or hyphens`);
|
|
50
|
+
if (names.has(step.capture as string)) fail(`${at}: the capture name ${step.capture as string} is used before`);
|
|
51
|
+
names.add(step.capture as string);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
|
|
55
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, "capture"].join(", ")}`);
|
|
56
|
+
const kind = kinds[0]!;
|
|
57
|
+
const allowed = ACTION_STEP_KEYS[kind] ?? [kind];
|
|
58
|
+
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
59
|
+
checkActionStep(kind, step, at, fail, false);
|
|
60
|
+
});
|
|
61
|
+
if (names.size === 0) fail("the steps have no capture step");
|
|
62
|
+
return value as LoadStep[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ranges from $0400 that the CPU wrote since the last capture, joined across
|
|
67
|
+
* gaps of up to `join` bytes, each with at least one changed byte. With
|
|
68
|
+
* `writesKnown`, every written byte counts, also one written with the value
|
|
69
|
+
* that was there (zeros over the zeros of VICE's start pattern, F-20).
|
|
70
|
+
* Without it, the KERNAL RAM test wrote all of RAM after the map was cleared,
|
|
71
|
+
* and only the changed bytes count.
|
|
72
|
+
*/
|
|
73
|
+
export function changedBlocks(before: Uint8Array, after: Uint8Array, map: Uint8Array, join: number, writesKnown: boolean): Array<{ start: number; end: number; bytes: number }> {
|
|
74
|
+
const blocks: Array<{ start: number; end: number; bytes: number; changed: boolean }> = [];
|
|
75
|
+
for (let address = FIRST_SEGMENT_ADDRESS; address < 0x10000; address++) {
|
|
76
|
+
const changed = before[address] !== after[address];
|
|
77
|
+
if ((map[address]! & COVERAGE.ramWrite) === 0 || (!writesKnown && !changed)) continue;
|
|
78
|
+
const last = blocks.at(-1);
|
|
79
|
+
if (last !== undefined && address - last.end <= join) {
|
|
80
|
+
last.end = address;
|
|
81
|
+
last.bytes++;
|
|
82
|
+
last.changed ||= changed;
|
|
83
|
+
} else blocks.push({ start: address, end: address, bytes: 1, changed });
|
|
84
|
+
}
|
|
85
|
+
return blocks.filter((block) => block.changed).map(({ start, end, bytes }) => ({ start, end, bytes }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hex4 = (value: number) => value.toString(16).padStart(4, "0");
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Runs the program and its steps. Each capture step writes <out>/<name>/:
|
|
92
|
+
* image.bin, image.json, entry_state.json, a PRG for each segment, and with
|
|
93
|
+
* `drive` the drive RAM as drive8.prg. Blocks below `minBlock` bytes are
|
|
94
|
+
* counted, not written: a game changes its variables all the time.
|
|
95
|
+
*/
|
|
96
|
+
export async function loads(program: string, options: { out: string; steps: LoadStep[]; drive: boolean; minBlock: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
97
|
+
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
98
|
+
try {
|
|
99
|
+
await client.reset({ mode: "hard", run: false });
|
|
100
|
+
let previous: Buffer;
|
|
101
|
+
/** A PRG that autostart put into memory without the CPU: its bytes count as written in the first capture. */
|
|
102
|
+
let loadedProgram: Buffer | undefined;
|
|
103
|
+
/** False while the map holds the writes of the KERNAL RAM test. */
|
|
104
|
+
let writesKnown = true;
|
|
105
|
+
/** A known flaw of the host's VICE version that a tape load meets (D56). */
|
|
106
|
+
let emulatorFlaw: string | undefined;
|
|
107
|
+
if (program.toLowerCase().endsWith(".prg")) {
|
|
108
|
+
const { bytes } = readProjectFile(program);
|
|
109
|
+
if (!(await autostartLoaded(client, program, bytes))) return { output: { loaded: false, reason: `The program was not in memory after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
|
|
110
|
+
// The RAM before RUN, without the program, so the first capture holds the program too.
|
|
111
|
+
previous = await readRam(client, 0x0000, 0x10000);
|
|
112
|
+
const load = bytes[0]! | (bytes[1]! << 8);
|
|
113
|
+
previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
|
|
114
|
+
loadedProgram = bytes;
|
|
115
|
+
await client.memmap({ action: "clear" });
|
|
116
|
+
// RUN as c64-testing types it: the steps start at the same time as a scenario's (D62).
|
|
117
|
+
if (!(await typeRun(client))) return { output: { loaded: false, reason: `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.` }, failed: true };
|
|
118
|
+
} else {
|
|
119
|
+
// A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
|
|
120
|
+
previous = await readRam(client, 0x0000, 0x10000);
|
|
121
|
+
await client.memmap({ action: "clear" });
|
|
122
|
+
({ emulatorFlaw } = await client.autostart({ path: program, index: 0, run: true }));
|
|
123
|
+
writesKnown = false;
|
|
124
|
+
if (!program.toLowerCase().endsWith(".crt")) {
|
|
125
|
+
// The map starts again where the KERNAL LOAD starts, after the RAM test: then it holds only what the program wrote.
|
|
126
|
+
const load = await client.runUntil({ target: { kind: "address", address: KERNAL_LOAD, space: "c64" }, timeoutFrames: LOAD_LIMIT_FRAMES });
|
|
127
|
+
if (load.reached) {
|
|
128
|
+
await client.memmap({ action: "clear" });
|
|
129
|
+
writesKnown = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const captures: Array<Record<string, unknown>> = [];
|
|
135
|
+
for (const [index, step] of options.steps.entries()) {
|
|
136
|
+
if (!("capture" in step)) {
|
|
137
|
+
const notReached = await runActionStep(client, step, parseC64Address);
|
|
138
|
+
if (notReached !== undefined && "runUntil" in step) {
|
|
139
|
+
const reason = `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
140
|
+
return { output: { loaded: false, reason, captures }, failed: true };
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
145
|
+
const registers = await client.registersGet("c64");
|
|
146
|
+
const memory = await readRam(client, 0x0000, 0x10000);
|
|
147
|
+
const map = await readCoverage(client);
|
|
148
|
+
if (loadedProgram !== undefined) {
|
|
149
|
+
// The PRG itself is part of the first capture.
|
|
150
|
+
const load = loadedProgram[0]! | (loadedProgram[1]! << 8);
|
|
151
|
+
for (let address = load; address < Math.min(0x10000, load + loadedProgram.length - 2); address++) map[address]! |= COVERAGE.ramWrite;
|
|
152
|
+
loadedProgram = undefined;
|
|
153
|
+
}
|
|
154
|
+
const blocks = changedBlocks(previous, memory, map, 16, writesKnown);
|
|
155
|
+
const segments = blocks.filter((block) => block.end - block.start + 1 >= options.minBlock);
|
|
156
|
+
const small = blocks.filter((block) => !segments.includes(block)).reduce((sum, block) => sum + block.bytes, 0);
|
|
157
|
+
const state = await machineState(client);
|
|
158
|
+
const vectors = vectorsOf(state, memory);
|
|
159
|
+
const directory = `${options.out}/${step.capture}`;
|
|
160
|
+
const write = (name: string, bytes: Uint8Array) => writeProjectFile(`${directory}/${name}`, bytes, { what: "--out" });
|
|
161
|
+
write("image.bin", memory);
|
|
162
|
+
const files = segments.map((segment) => {
|
|
163
|
+
const name = `segment-${hex4(segment.start)}-${hex4(segment.end)}.prg`;
|
|
164
|
+
write(name, Buffer.concat([Buffer.from([segment.start & 0xff, segment.start >> 8]), memory.subarray(segment.start, segment.end + 1)]));
|
|
165
|
+
return name;
|
|
166
|
+
});
|
|
167
|
+
let drive: Record<string, unknown> | undefined;
|
|
168
|
+
if (options.drive) {
|
|
169
|
+
const ram = Buffer.from((await client.memoryRead({ address: 0x0000, size: DRIVE_RAM, space: "drive8", view: "cpu" })).data, "hex");
|
|
170
|
+
write("drive8.prg", Buffer.concat([Buffer.from([0x00, 0x00]), ram]));
|
|
171
|
+
drive = { file: "drive8.prg", pc: formatC64Address((await client.registersGet("drive8")).pc), space: "drive8" };
|
|
172
|
+
}
|
|
173
|
+
const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
174
|
+
write(
|
|
175
|
+
"image.json",
|
|
176
|
+
json({
|
|
177
|
+
capture: step.capture,
|
|
178
|
+
entry: formatC64Address(registers.pc),
|
|
179
|
+
segments: segments.map((segment, at) => ({ ...showRange(segment), file: files[at] })),
|
|
180
|
+
vectors,
|
|
181
|
+
image: "image.bin",
|
|
182
|
+
...(drive === undefined ? {} : { drive }),
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
write("entry_state.json", json(entryState(registers, state, vectors)));
|
|
186
|
+
const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
|
|
187
|
+
captures.push({
|
|
188
|
+
capture: step.capture,
|
|
189
|
+
out: directory,
|
|
190
|
+
pc: formatC64Address(registers.pc),
|
|
191
|
+
...(keyWait ? { keyWait: true } : {}),
|
|
192
|
+
segments: segments.map(showRange),
|
|
193
|
+
...(small === 0 ? {} : { smallChanges: small }),
|
|
194
|
+
...(drive === undefined ? {} : { drive: drive.file }),
|
|
195
|
+
});
|
|
196
|
+
previous = memory;
|
|
197
|
+
await client.memmap({ action: "clear" });
|
|
198
|
+
writesKnown = true;
|
|
199
|
+
}
|
|
200
|
+
if (captures.length > 0 && captures.every((capture) => (capture.segments as unknown[]).length === 0)) {
|
|
201
|
+
const waits = captures.some((capture) => capture.keyWait === true) ? " The C64 waited for a key at READY. or at an INPUT statement, so the load ended or did not start." : "";
|
|
202
|
+
const reason = `No capture has a segment: the program changed no block of RAM from ${formatC64Address(FIRST_SEGMENT_ADDRESS)} on.${waits}`;
|
|
203
|
+
return { output: { loaded: false, reason, captures, ...(emulatorFlaw === undefined ? {} : { emulatorFlaw }) }, failed: true };
|
|
204
|
+
}
|
|
205
|
+
return { output: { loaded: true, captures }, failed: false };
|
|
206
|
+
} finally {
|
|
207
|
+
await client.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The machine state that a capture writes (D43): RAM, the CPU port, pages
|
|
2
|
+
// 0-3, the chip registers read without side effects, and the vectors. The
|
|
3
|
+
// capture and loads commands of the unpacker share it.
|
|
4
|
+
|
|
5
|
+
import { formatC64Address } from "#src/c64.ts";
|
|
6
|
+
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
7
|
+
|
|
8
|
+
type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
|
|
9
|
+
|
|
10
|
+
const MAX_READ = 4096;
|
|
11
|
+
|
|
12
|
+
/** The I/O areas of the entry state, read without side effects (D43). */
|
|
13
|
+
export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc00, 0x10], cia2: [0xdd00, 0x10] } as const;
|
|
14
|
+
/** Frames from a hard reset to the READY prompt, with margin. */
|
|
15
|
+
export const READY_FRAMES = 200;
|
|
16
|
+
|
|
17
|
+
/** What no register read of a CIA gives (D63). */
|
|
18
|
+
export interface CiaHidden {
|
|
19
|
+
latchA: number;
|
|
20
|
+
latchB: number;
|
|
21
|
+
interruptMask: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MachineState {
|
|
25
|
+
port: Buffer;
|
|
26
|
+
pages: Buffer;
|
|
27
|
+
io: Record<keyof typeof IO_AREAS, Buffer>;
|
|
28
|
+
/** The timer latches and the interrupt mask of each CIA. */
|
|
29
|
+
cia: { cia1: CiaHidden; cia2: CiaHidden };
|
|
30
|
+
romVectors: Buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reads `size` bytes from `address` in the RAM view, in parts. */
|
|
34
|
+
export async function readRam(client: ViceSessionClient, address: number, size: number): Promise<Buffer> {
|
|
35
|
+
const parts: Buffer[] = [];
|
|
36
|
+
for (let at = address; at < address + size; at += MAX_READ) {
|
|
37
|
+
parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_READ, address + size - at), space: "c64", view: "ram" })).data, "hex"));
|
|
38
|
+
}
|
|
39
|
+
return Buffer.concat(parts);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The state the program finds at its entry: what a rebuild's loader must recreate. */
|
|
43
|
+
export async function machineState(client: ViceSessionClient): Promise<MachineState> {
|
|
44
|
+
const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
|
|
45
|
+
const pages = await readRam(client, 0x0000, 0x400);
|
|
46
|
+
const io = {} as Record<keyof typeof IO_AREAS, Buffer>;
|
|
47
|
+
for (const [name, [address, size]] of Object.entries(IO_AREAS)) io[name as keyof typeof IO_AREAS] = Buffer.from((await client.ioRead(address, size)).data, "hex");
|
|
48
|
+
const [cia1, cia2] = (await client.cia("both")).chips.map(({ latchA, latchB, interruptMask }) => ({ latchA, latchB, interruptMask }));
|
|
49
|
+
const romVectors = Buffer.from((await client.memoryRead({ address: 0xfffa, size: 6, space: "c64", view: "cpu" })).data, "hex");
|
|
50
|
+
return { port, pages, io, cia: { cia1: cia1!, cia2: cia2! }, romVectors };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const word = (bytes: Buffer, offset: number) => formatC64Address(bytes[offset]! | (bytes[offset + 1]! << 8));
|
|
54
|
+
|
|
55
|
+
/** The KERNAL vectors in page 3, the hardware vectors in RAM, and the ROM's own. */
|
|
56
|
+
export function vectorsOf(state: MachineState, memory: Buffer) {
|
|
57
|
+
return {
|
|
58
|
+
irq: word(state.pages, 0x314),
|
|
59
|
+
brk: word(state.pages, 0x316),
|
|
60
|
+
nmi: word(state.pages, 0x318),
|
|
61
|
+
ram: { nmi: word(memory, 0xfffa), reset: word(memory, 0xfffc), irq: word(memory, 0xfffe) },
|
|
62
|
+
rom: { nmi: word(state.romVectors, 0), reset: word(state.romVectors, 2), irq: word(state.romVectors, 4) },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The content of entry_state.json; `differsFromReset` only when the caller compared with a clean reset. */
|
|
67
|
+
export function entryState(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, differsFromReset?: Array<{ address: string; entry: number; reset: number }>) {
|
|
68
|
+
return {
|
|
69
|
+
registers: { pc: formatC64Address(registers.pc), a: registers.a, x: registers.x, y: registers.y, sp: registers.sp, flags: registers.flags },
|
|
70
|
+
cpuPort: { $00: state.port[0], $01: state.port[1] },
|
|
71
|
+
pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
|
|
72
|
+
io: Object.fromEntries(Object.entries(IO_AREAS).map(([name, [address]]) => [name, { from: formatC64Address(address), bytes: state.io[name as keyof typeof IO_AREAS].toString("hex") }])),
|
|
73
|
+
// The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them (D63).
|
|
74
|
+
cia: state.cia,
|
|
75
|
+
vectors,
|
|
76
|
+
...(differsFromReset === undefined ? {} : { differsFromReset }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const json = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|