@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -11,26 +11,30 @@ Find the static structure of a C64 program. Record the structure in the project
|
|
|
11
11
|
|
|
12
12
|
## Inputs
|
|
13
13
|
|
|
14
|
-
- The program
|
|
14
|
+
- The program in the knowledge. Put it in with `image` of the c64-knowledge skill: a PRG, or a capture of the c64-unpacker skill.
|
|
15
|
+
- Optional: the coverage in the knowledge. Add it with `coverage` of the c64-knowledge skill.
|
|
15
16
|
- Entry points: addresses where code starts, for example the SYS address from the c64-basic skill. Routine symbols in the knowledge are entry points too. Ghidra needs at least one. DXA finds the SYS of a BASIC start by itself.
|
|
16
17
|
- Optional: routines to decompile (Ghidra), or a file for the listing (DXA).
|
|
17
18
|
|
|
18
19
|
## Tools / execution path
|
|
19
20
|
|
|
20
|
-
The script reads the current knowledge and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in `.c64-re-tools/knowledge
|
|
21
|
+
The script reads the program, the coverage and the current knowledge, and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in the project knowledge, `.c64-re-tools/knowledge/`.
|
|
21
22
|
|
|
22
23
|
```
|
|
23
|
-
node <skill>/scripts/analyze.ts
|
|
24
|
-
node <skill>/scripts/analyze.ts
|
|
24
|
+
node <skill>/scripts/analyze.ts --analyzer dxa --listing .c64-re-tools/local/game.lst
|
|
25
|
+
node <skill>/scripts/analyze.ts --entry '$080d' --decompile '$080d'
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
| Option | Meaning |
|
|
28
29
|
|---|---|
|
|
29
30
|
| `--analyzer dxa` | Use DXA: a fast first pass. Without this option, the script uses Ghidra. |
|
|
30
31
|
| `--entry '$080d'` | An address where code starts. Use it again for more addresses. |
|
|
31
|
-
| `--listing
|
|
32
|
+
| `--listing .c64-re-tools/local/game.lst` | DXA only: write the disassembly listing to this project file. |
|
|
32
33
|
| `--decompile '$0818'` | Ghidra only: decompile the routine at this address. Use it again for more routines, at most 32. |
|
|
33
|
-
| `--
|
|
34
|
+
| `--max-listed 20` | List at most this number of labels, functions or candidates. The default is 50. Use 0 to list none. |
|
|
35
|
+
| `--instructions .c64-re-tools/local/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. When the knowledge has coverage, `executed` tells if it ran. |
|
|
36
|
+
| `--space drive8` | The knowledge space of the program: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
|
|
37
|
+
| `--macro-batch .c64-re-tools/local/macros.json` | Write the macro candidates to this project file as a batch for `apply` of the c64-knowledge skill. |
|
|
34
38
|
|
|
35
39
|
The script prints one JSON object. Addresses are `$` and four hex digits.
|
|
36
40
|
|
|
@@ -42,6 +46,25 @@ Both analyzers use the knowledge from the user and the LLM as seeds:
|
|
|
42
46
|
|
|
43
47
|
Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
|
|
44
48
|
|
|
49
|
+
More seeds come from evidence:
|
|
50
|
+
|
|
51
|
+
- The start of each `code` region and each entry of a `jumps` table from the user or the LLM is an entry point.
|
|
52
|
+
- When the knowledge has coverage, each executed instruction of the program that starts a flow is an entry point. A flow starts after a gap, or after RTS, RTI, JMP, BRK or JSR. Then the analyzers find code that only a vector, an RTS table or an interrupt reaches. They also find the code after a JSR to a routine that an analyzer thinks does not return.
|
|
53
|
+
- For Ghidra, a flow start is a routine only when no executed branch or JMP from another flow gets to it. Also, no JSR can be immediately before it. Another flow start is code in a routine, and Ghidra does not make a function of it.
|
|
54
|
+
- When the knowledge has coverage, Ghidra gives `coverageGaps`: the executed instruction starts in the program that are not in its instructions. Normally the count is 0. Each start in the list is missing from `--instructions` and from the source of c64-source. Examine the code at the start. Overlapped instructions and self-modified code can cause a gap. A start in a `dual-use` region is a known overlap, for example a BIT skip, and is not in the list.
|
|
55
|
+
- When the knowledge has coverage, some RAM under a ROM is data for the analysis. At these addresses, the runs used the ROM and did not read or execute the RAM. Then a call into the KERNAL does not decode the RAM bytes under it as code. A knowledge routine or an `--entry` in such a range keeps its code. Ghidra does not follow BRK, because the vector at `$fffe` is in ROM unless the program removes the KERNAL.
|
|
56
|
+
|
|
57
|
+
The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. Ghidra also takes at most 8192 flow starts that are code in a routine. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used. `romOnlyRanges` and `romOnlyUsed` give the same counts for the data ranges under ROM.
|
|
58
|
+
|
|
59
|
+
### Project folder
|
|
60
|
+
|
|
61
|
+
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
62
|
+
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
63
|
+
- Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
|
|
64
|
+
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
|
|
65
|
+
- Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
|
|
66
|
+
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
67
|
+
|
|
45
68
|
## Workflow
|
|
46
69
|
|
|
47
70
|
1. Read the knowledge for the program with the c64-knowledge skill.
|
|
@@ -49,10 +72,16 @@ Facts that an analyzer found are not seeds. A new analysis can then correct or r
|
|
|
49
72
|
3. Find the important routines. Give them names with the c64-knowledge skill.
|
|
50
73
|
4. Run a deeper pass with Ghidra. Give the entry points with `--entry`. Ask for decompiled code of the routines that you must understand.
|
|
51
74
|
5. Read the result:
|
|
52
|
-
- `changes` counts the
|
|
53
|
-
- DXA: `regions` lists the code and data regions
|
|
54
|
-
- Ghidra: `functions`
|
|
75
|
+
- `changes` counts the analyzer names (`names`), regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `symbols.retired` counts the old symbols of the analyzer that moved to its analyzer names. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
|
|
76
|
+
- DXA: `regions` lists the code and data regions. `labels` gives the number of names that DXA found (`count`) and lists the first names (`listed`).
|
|
77
|
+
- Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
|
|
78
|
+
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
|
|
55
79
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
80
|
+
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
81
|
+
- `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
|
|
82
|
+
- `candidates.pitfalls` gives code that a C64 runs in a different way than it reads. `raster-bit-8`: the instruction at `at` reads $D011, and the instruction at `store` writes the value back. A read of $D011 gives bit 8 of the raster line in bit 7. Thus the write can move the raster interrupt to line 256 or later, and on PAL past line 311 the interrupt never comes. Examine each candidate. Where the code must not keep bit 7, add a comment and test the program.
|
|
83
|
+
- `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--macro-batch`, `batch` gives the file, the count of writes and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
|
|
84
|
+
- When the knowledge has 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. For many conflicts, write all classifications in one `apply` batch of the c64-knowledge skill.
|
|
56
85
|
6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
|
|
57
86
|
|
|
58
87
|
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.
|
|
@@ -61,12 +90,18 @@ The script records only structure: generated names, code and data regions, and r
|
|
|
61
90
|
|
|
62
91
|
Automatic write:
|
|
63
92
|
- code and data regions
|
|
64
|
-
- names that DXA generated
|
|
93
|
+
- analyzer names: the labels that DXA generated and the functions that Ghidra found, for example `l17e2` and `FUN_17e2`
|
|
65
94
|
- references from Ghidra: call, jump, read and write.
|
|
66
95
|
|
|
67
|
-
|
|
96
|
+
An analyzer name is a second name of the address. It is not a symbol. Each analyzer keeps its own name at an address. The generated source does not use an analyzer name, unless you give `--analyzer-names` to the c64-source skill. To use a name in the source, give it to the address with `rename` of the c64-knowledge skill. A Ghidra function that only branches and jumps get to is a `label`, not a `routine`.
|
|
97
|
+
|
|
98
|
+
The script never replaces a name or a region from the user or the LLM. DXA and Ghidra do not replace the facts of each other, with one exception. When the knowledge has coverage, a code finding replaces a DXA or Ghidra data region where a run executed an instruction start. Thus add all coverage maps to the knowledge first. Then run DXA, then Ghidra.
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
A new analysis replaces the earlier facts of the same analyzer in the analyzed range, also its analyzer names. The history keeps the earlier facts. Knowledge from an older version can have analyzer names as symbols. A new analysis moves the names of that analyzer to its analyzer names.
|
|
68
103
|
|
|
69
|
-
A
|
|
104
|
+
A fact that the user or the LLM removes does not come back. The next analysis does not write a fact at that place, and counts it as `suppressed`. To remove a wrong fact, use the c64-knowledge skill.
|
|
70
105
|
|
|
71
106
|
Do not store:
|
|
72
107
|
- the listing or decompiled code
|
|
@@ -82,10 +117,15 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
|
|
|
82
117
|
|
|
83
118
|
- A conflict: the analyzer finds code where the knowledge has data, or data where the knowledge has code. The knowledge stays unchanged. Find the correct answer with the c64-emulator skill, then correct the knowledge with the c64-knowledge skill.
|
|
84
119
|
- A conflict with the problem "another address has this name": the knowledge has the generated name at a different address. The knowledge stays unchanged.
|
|
120
|
+
- A false reference through a vector: the code jumps through a vector, for example `JMP ($0408)`, and the program writes the vector at run time. The analyzer follows the bytes that the program has there. Do these steps:
|
|
121
|
+
1. Find the real targets with the c64-emulator skill, or in a coverage file.
|
|
122
|
+
2. If the vector bytes are also code at another time, classify them as `dual-use`.
|
|
123
|
+
3. Remove the false reference with `unreference`. It does not come back.
|
|
124
|
+
4. Add a `jump` reference from the JMP to each real target, and name each target as a routine.
|
|
85
125
|
- `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
|
|
86
126
|
- `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
|
|
87
127
|
- `operation-failed` with a message about an incomplete result or listing: the analyzer did not give a complete result. The script recorded nothing. Tell the user.
|
|
88
|
-
- `installation-incomplete
|
|
128
|
+
- `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.
|
|
89
129
|
- `notDecompiled`: Ghidra did not decompile these routines. Each entry has the reason. If the reason says that the decompiler did not start, tell the user the remedy in the reason. On macOS, Ghidra has no native decompiler until the user builds it: `./gradlew buildNatives` in the `support/gradle` directory of Ghidra.
|
|
90
130
|
|
|
91
131
|
## Handoffs
|
|
@@ -1,39 +1,151 @@
|
|
|
1
|
-
// The c64-static-analysis script: analyzes
|
|
2
|
-
// first pass) or Ghidra (deeper), which it runs itself, seeded
|
|
3
|
-
// current knowledge
|
|
4
|
-
// .c64-re-tools/knowledge
|
|
1
|
+
// The c64-static-analysis script: analyzes the program in the knowledge (D83)
|
|
2
|
+
// with DXA (fast first pass) or Ghidra (deeper), which it runs itself, seeded
|
|
3
|
+
// from current knowledge and the coverage of the knowledge, and imports the
|
|
4
|
+
// structural findings into .c64-re-tools/knowledge/.
|
|
5
5
|
|
|
6
6
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
10
|
-
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
|
+
import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
|
|
11
11
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { writeProjectFile } from "#src/host-client/transfer.ts";
|
|
13
13
|
import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
|
|
14
14
|
import { analyze as analyzeWithGhidra } from "#src/native/ghidra/analyze.ts";
|
|
15
15
|
import { localToolContext } from "#src/native/local.ts";
|
|
16
|
+
import { GHIDRA_LIMITS } from "#src/native/types.ts";
|
|
16
17
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
18
|
+
import { analyzedBytes, programCoverage, requireProgram } from "#src/knowledge/program.ts";
|
|
17
19
|
import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
|
|
18
|
-
import { currentRevision } from "#src/knowledge/read.ts";
|
|
20
|
+
import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
21
|
+
import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
|
|
22
|
+
import { platformNames } from "#src/platform.ts";
|
|
19
23
|
import { resolveProjectPath } from "#src/project.ts";
|
|
20
24
|
import { runScript, UsageError } from "#src/script.ts";
|
|
21
|
-
import {
|
|
25
|
+
import { codeEntries, coverageConflicts, coverageEntries, coverageSeedKinds, executedStarts, imageReader, romOnlyRanges, type ImageByte } from "./lib/coverage.ts";
|
|
26
|
+
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./lib/findings.ts";
|
|
27
|
+
import { findIdioms } from "./lib/idioms.ts";
|
|
28
|
+
import { instructionList } from "./lib/instructions.ts";
|
|
29
|
+
import { CATALOGUE, findMacroUses, type MacroCandidate } from "./lib/macros.ts";
|
|
30
|
+
import { findPitfalls, type Pitfall } from "./lib/pitfalls.ts";
|
|
31
|
+
import { findPointers, type PointerCandidate } from "./lib/pointers.ts";
|
|
22
32
|
|
|
23
|
-
const USAGE = `analyze.ts
|
|
33
|
+
const USAGE = `analyze.ts [options]
|
|
34
|
+
|
|
35
|
+
The program and the coverage come from the knowledge (D83): put them in with
|
|
36
|
+
knowledge.ts image and coverage of the c64-knowledge skill. The executed code of
|
|
37
|
+
the coverage is seeded, and analyzer data over it is reported.
|
|
24
38
|
|
|
25
|
-
<image> a PRG file, relative to the project directory
|
|
26
39
|
--analyzer <name> dxa (fast first pass) or ghidra (deeper, the default)
|
|
27
|
-
--flat64k the image is 64 KiB of memory from $0000, not a PRG
|
|
28
40
|
--entry <address> an entry point, for example '$080d' (repeatable)
|
|
29
41
|
--decompile <address> ghidra: a routine to decompile, at most 32 (repeatable)
|
|
30
42
|
--listing <file> dxa: write the disassembly listing to this project file
|
|
43
|
+
--instructions <file> ghidra: write every instruction (address, bytes, mnemonic,
|
|
44
|
+
mode, operand, target, executed) to this project file as JSON
|
|
45
|
+
--max-listed <n> list at most n labels, functions or candidates (default 50,
|
|
46
|
+
0 for none); the counts are always given, and knowledge has
|
|
47
|
+
all labels and functions
|
|
48
|
+
--space <space> the knowledge space of the program: c64 (the default), drive8
|
|
49
|
+
for 1541 drive code, or bank0 to bank255 for a cartridge bank
|
|
50
|
+
--macro-batch <file> write the macro candidates as an apply batch of c64-knowledge
|
|
51
|
+
(the macros and their uses) to this project file
|
|
31
52
|
|
|
32
53
|
Routine symbols in knowledge are entry points too. Ghidra needs at least one
|
|
33
54
|
entry point; dxa finds the SYS of a BASIC start by itself. The result is one
|
|
34
55
|
JSON object. Addresses are $ and four hex digits.`;
|
|
35
56
|
|
|
36
|
-
const
|
|
57
|
+
const DEFAULT_MAX_LISTED = 50;
|
|
58
|
+
|
|
59
|
+
/** The first `max` items, the count, and where to read the rest when the list is cut. */
|
|
60
|
+
function listed<T, U>(items: T[], max: number, show: (item: T) => U, all: string) {
|
|
61
|
+
return { count: items.length, listed: items.slice(0, max).map(show), ...(items.length > max ? { rest: all } : {}) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A pitfall with $ addresses and what goes wrong (M23.7). */
|
|
65
|
+
function showPitfall(pitfall: Pitfall) {
|
|
66
|
+
return {
|
|
67
|
+
kind: pitfall.kind,
|
|
68
|
+
at: formatC64Address(pitfall.at),
|
|
69
|
+
store: formatC64Address(pitfall.store),
|
|
70
|
+
problem: "A read of $D011 gives raster bit 8 in bit 7; the write sets bit 8 of the raster compare. Clear bit 7 (AND #$7F) unless the code wants a compare at line 256 or later.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A pointer candidate with $ addresses, and the operand writes that would name it (D65). */
|
|
75
|
+
function showPointer(candidate: PointerCandidate) {
|
|
76
|
+
const hex = formatC64Address;
|
|
77
|
+
if (candidate.kind === "sprite pointer") {
|
|
78
|
+
return { kind: candidate.kind, at: hex(candidate.at), to: hex(candidate.to), value: `$${candidate.value.toString(16).padStart(2, "0")}`, target: hex(candidate.target), assumes: "VIC bank 0 and the screen at $0400" };
|
|
79
|
+
}
|
|
80
|
+
return { kind: candidate.kind, to: hex(candidate.to), target: hex(candidate.target), low: hex(candidate.low), high: hex(candidate.high) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A macro candidate with $ addresses and its source line (D73). */
|
|
84
|
+
function showMacro(candidate: MacroCandidate) {
|
|
85
|
+
return { macro: candidate.macro, start: formatC64Address(candidate.start), end: formatC64Address(candidate.end), args: candidate.args, text: `+${candidate.macro} ${candidate.args.join(", ")}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The macro candidates (D73) in the current code regions of every origin,
|
|
90
|
+
* with the names of the knowledge in their args. A candidate is left out when
|
|
91
|
+
* a name or a reference target is inside it, or a patch or a use overlaps it:
|
|
92
|
+
* c64-source would write its instructions. The same use in the knowledge is
|
|
93
|
+
* known. With `batchPath`, the candidates go to an apply batch with the
|
|
94
|
+
* macros they need; a macro of the catalogue that the knowledge has with
|
|
95
|
+
* another body leaves its candidates out of the batch.
|
|
96
|
+
*/
|
|
97
|
+
function macroCandidates(space: string, byteAt: ImageByte, batchPath: string | undefined) {
|
|
98
|
+
const db = openForRead();
|
|
99
|
+
try {
|
|
100
|
+
const platform = space === DEFAULT_SPACE ? new Map(platformNames().map((entry) => [entry.address, entry.name])) : new Map<number, string>();
|
|
101
|
+
const semantic = new Map(listSymbols(db, undefined, undefined, ["user", "llm"], space).map((symbol) => [symbol.address, symbol.name]));
|
|
102
|
+
const code = listRegions(db, undefined, "code", undefined, space);
|
|
103
|
+
const found = findMacroUses(byteAt, code, (address) => semantic.get(address) ?? platform.get(address));
|
|
104
|
+
const named = new Set([...listSymbols(db, undefined, undefined, undefined, space).map((symbol) => symbol.address), ...listReferences(db, undefined, undefined, undefined, space).map((reference) => reference.to)]);
|
|
105
|
+
const patches = listPatches(db, undefined, undefined, undefined, space);
|
|
106
|
+
const uses = listMacroUses(db, undefined, undefined, undefined, space);
|
|
107
|
+
const overlaps = (a: { start: number; end: number }, b: { start: number; end: number }) => a.start <= b.end && b.start <= a.end;
|
|
108
|
+
const candidates: MacroCandidate[] = [];
|
|
109
|
+
let known = 0;
|
|
110
|
+
let leftOut = 0;
|
|
111
|
+
for (const candidate of found) {
|
|
112
|
+
const same = uses.find((use) => use.start === candidate.start && use.end === candidate.end && use.macro === candidate.macro && use.args.join(",") === candidate.args.join(","));
|
|
113
|
+
if (same !== undefined) known++;
|
|
114
|
+
else if ([...named].some((address) => address > candidate.start && address <= candidate.end) || patches.some((patch) => overlaps(patch, candidate)) || uses.some((use) => overlaps(use, candidate))) leftOut++;
|
|
115
|
+
else candidates.push(candidate);
|
|
116
|
+
}
|
|
117
|
+
if (batchPath === undefined) return { candidates, known, leftOut };
|
|
118
|
+
// A macro of the catalogue goes into the batch unless the knowledge has it; with another body, its uses stay out.
|
|
119
|
+
const macros = new Map(listMacros(db, undefined, space).map((macro) => [macro.name, macro]));
|
|
120
|
+
const batch: Array<Record<string, unknown>> = [];
|
|
121
|
+
const batched: MacroCandidate[] = [];
|
|
122
|
+
const other = new Set<string>();
|
|
123
|
+
for (const idiom of CATALOGUE.filter((item) => candidates.some((candidate) => candidate.macro === item.name))) {
|
|
124
|
+
const existing = macros.get(idiom.name);
|
|
125
|
+
if (existing === undefined) batch.push({ op: "macro", name: idiom.name, params: idiom.params, body: idiom.body, comment: idiom.comment, ...(space === DEFAULT_SPACE ? {} : { space }) });
|
|
126
|
+
else if (existing.params.join(",") !== idiom.params.join(",") || existing.body !== idiom.body) other.add(idiom.name);
|
|
127
|
+
}
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
if (other.has(candidate.macro)) continue;
|
|
130
|
+
batched.push(candidate);
|
|
131
|
+
batch.push({ op: "use", start: formatC64Address(candidate.start), end: formatC64Address(candidate.end), macro: candidate.macro, args: candidate.args, ...(space === DEFAULT_SPACE ? {} : { space }) });
|
|
132
|
+
}
|
|
133
|
+
if (batch.length > 0) writeProjectFile(batchPath, Buffer.from(`${JSON.stringify(batch, null, 1)}\n`), { what: "--macro-batch" });
|
|
134
|
+
return {
|
|
135
|
+
candidates,
|
|
136
|
+
known,
|
|
137
|
+
leftOut,
|
|
138
|
+
batch: {
|
|
139
|
+
out: batchPath,
|
|
140
|
+
writes: batch.length,
|
|
141
|
+
...(other.size === 0 ? {} : { otherMacros: [...other].map((name) => `The knowledge has another macro ${name}: its candidates are not in the batch.`) }),
|
|
142
|
+
...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${batchPath} --reason "macro uses of 6502 idioms"` }),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
} finally {
|
|
146
|
+
db?.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
37
149
|
|
|
38
150
|
/** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
|
|
39
151
|
function withRemedy(reason: string): string {
|
|
@@ -72,10 +184,10 @@ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<stri
|
|
|
72
184
|
|
|
73
185
|
/**
|
|
74
186
|
* Imports one analyzer's findings as one revision, refused when knowledge
|
|
75
|
-
* changed since the seeds were read. A refused import creates no knowledge.
|
|
187
|
+
* changed since the seeds were read. A refused import creates no knowledge.
|
|
76
188
|
* The revision records the image hash and the analyzer version.
|
|
77
189
|
*/
|
|
78
|
-
function record(findings: NormalizedFindings, revision:
|
|
190
|
+
function record(findings: NormalizedFindings, revision: string, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
|
|
79
191
|
let imported: ImportResult;
|
|
80
192
|
try {
|
|
81
193
|
imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
|
|
@@ -87,7 +199,7 @@ function record(findings: NormalizedFindings, revision: number, description: str
|
|
|
87
199
|
}
|
|
88
200
|
return {
|
|
89
201
|
revision: imported.revision,
|
|
90
|
-
changes: { symbols: imported.symbols, regions: imported.regions, references: imported.references },
|
|
202
|
+
changes: { names: imported.names, ...(imported.symbols.retired === 0 ? {} : { symbols: { retired: imported.symbols.retired } }), regions: imported.regions, references: imported.references },
|
|
91
203
|
conflicts: imported.conflicts.map((conflict) => showConflict(conflict, findings.analyzer)),
|
|
92
204
|
};
|
|
93
205
|
}
|
|
@@ -99,44 +211,139 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
99
211
|
allowPositionals: true,
|
|
100
212
|
options: {
|
|
101
213
|
analyzer: { type: "string" },
|
|
102
|
-
flat64k: { type: "boolean" },
|
|
103
214
|
entry: { type: "string", multiple: true },
|
|
104
215
|
decompile: { type: "string", multiple: true },
|
|
105
216
|
listing: { type: "string" },
|
|
217
|
+
"max-listed": { type: "string" },
|
|
218
|
+
instructions: { type: "string" },
|
|
219
|
+
space: { type: "string" },
|
|
220
|
+
"macro-batch": { type: "string" },
|
|
106
221
|
help: { type: "boolean" },
|
|
107
222
|
},
|
|
108
223
|
});
|
|
109
224
|
if (values.help) throw new UsageError("");
|
|
110
|
-
if (positionals.length !==
|
|
111
|
-
const image = positionals[0]!;
|
|
225
|
+
if (positionals.length !== 0) throw new UsageError("analyze takes no image: it reads the program from the knowledge (knowledge.ts image of the c64-knowledge skill puts it in)");
|
|
112
226
|
const entries = addresses(values.entry, "--entry");
|
|
113
227
|
const decompile = addresses(values.decompile, "--decompile");
|
|
114
228
|
if (decompile.length > 32) throw new UsageError("--decompile takes at most 32 routines");
|
|
229
|
+
const maxListed = values["max-listed"] ?? String(DEFAULT_MAX_LISTED);
|
|
230
|
+
if (!/^\d{1,5}$/.test(maxListed)) throw new UsageError(`--max-listed must be a whole number, not ${maxListed}`);
|
|
231
|
+
const max = Number(maxListed);
|
|
115
232
|
const analyzer = values.analyzer ?? "ghidra";
|
|
116
233
|
if (analyzer !== "dxa" && analyzer !== "ghidra") throw new UsageError(`--analyzer must be dxa or ghidra, not ${analyzer}`);
|
|
117
234
|
if (analyzer === "dxa" && decompile.length > 0) throw new UsageError("--decompile works only with --analyzer ghidra");
|
|
118
235
|
if (analyzer === "ghidra" && values.listing !== undefined) throw new UsageError("--listing works only with --analyzer dxa");
|
|
236
|
+
if (analyzer === "dxa" && values.instructions !== undefined) throw new UsageError("--instructions works only with --analyzer ghidra");
|
|
119
237
|
let listingPath: string | undefined;
|
|
120
238
|
if (values.listing !== undefined) {
|
|
121
239
|
try {
|
|
122
240
|
listingPath = resolveProjectPath(values.listing);
|
|
123
241
|
} catch {
|
|
124
|
-
throw new UsageError(`--listing must be a path relative to the project
|
|
242
|
+
throw new UsageError(`--listing must be a path relative to the project folder that stays inside it, not ${values.listing}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (values["macro-batch"] !== undefined) {
|
|
247
|
+
try {
|
|
248
|
+
resolveProjectPath(values["macro-batch"]);
|
|
249
|
+
} catch {
|
|
250
|
+
throw new UsageError(`--macro-batch must be a path relative to the project folder that stays inside it, not ${values["macro-batch"]}`);
|
|
125
251
|
}
|
|
126
252
|
}
|
|
127
253
|
|
|
254
|
+
const space = values.space ?? DEFAULT_SPACE;
|
|
255
|
+
if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
128
256
|
const reader = openForRead();
|
|
257
|
+
const program = requireProgram(reader, space);
|
|
258
|
+
// A coverage map is of the C64 CPU: VICE keeps none for the drive (D53).
|
|
259
|
+
const covered = space === DEFAULT_SPACE ? programCoverage(reader) : undefined;
|
|
260
|
+
const coverage = covered?.map;
|
|
261
|
+
const { bytes, kind: imageKind } = analyzedBytes(program);
|
|
262
|
+
// The analyzer sees all bytes it gets; the coverage seeds only the program, as c64-source does.
|
|
263
|
+
const byteAt = imageReader(bytes, imageKind);
|
|
264
|
+
const inProgram: ImageByte = (address) => (program.segments.some((segment) => address >= segment.start && address <= segment.end) ? program.byteAt(address) : undefined);
|
|
265
|
+
// With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58).
|
|
266
|
+
const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
|
|
267
|
+
const loaded = imageKind === "flat64k" ? { start: 0x0000, end: 0xffff } : prgRange(bytes);
|
|
268
|
+
const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, covered);
|
|
269
|
+
|
|
129
270
|
const revision = currentRevision(reader);
|
|
130
|
-
const seeds = seedsFromKnowledge(reader, entries);
|
|
271
|
+
const seeds = seedsFromKnowledge(reader, entries, space);
|
|
272
|
+
// Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound (D44).
|
|
273
|
+
const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, inProgram);
|
|
274
|
+
const known = [...seeds.entryPoints, ...codeEntries(reader, byteAt, space)];
|
|
275
|
+
// Ghidra takes the coverage as code starts without a function (D74); DXA has one list.
|
|
276
|
+
const kinds = analyzer === "dxa" || coverage === undefined ? { routines: fromCoverage, codeStarts: [] } : coverageSeedKinds(coverage, inProgram);
|
|
277
|
+
const ordered = [...new Set([...known, ...idioms.entryPoints, ...kinds.routines])];
|
|
278
|
+
seeds.entryPoints = ordered.slice(0, GHIDRA_LIMITS.entryPoints).sort((a, b) => a - b);
|
|
279
|
+
const codeStarts = kinds.codeStarts.filter((entry) => !seeds.entryPoints.includes(entry)).slice(0, GHIDRA_LIMITS.codeStarts);
|
|
280
|
+
// Idiom tables and the RAM under ROM that the runs never used are data for this run too; semantic data ranges come first.
|
|
281
|
+
// A range with an entry point of the knowledge or --entry in it is left out: that code stays code.
|
|
282
|
+
const fromRom = coverage === undefined ? [] : romOnlyRanges(coverage, byteAt).filter((range) => !known.some((entry) => entry >= range.start && entry <= range.end));
|
|
283
|
+
const dataRanges = [...seeds.dataRanges];
|
|
284
|
+
for (const range of [...idioms.dataRanges, ...fromRom]) if (!dataRanges.some((seed) => range.start <= seed.end && range.end >= seed.start)) dataRanges.push(range);
|
|
285
|
+
seeds.dataRanges = dataRanges.slice(0, GHIDRA_LIMITS.dataRanges);
|
|
286
|
+
const romUsed = fromRom.filter((range) => seeds.dataRanges.includes(range)).length;
|
|
287
|
+
const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry) || codeStarts.includes(entry)).length;
|
|
288
|
+
// Platform names are C64 names: drive code and banks get none.
|
|
289
|
+
const platform = space === DEFAULT_SPACE ? platformNames() : [];
|
|
290
|
+
const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, {
|
|
291
|
+
platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
|
|
292
|
+
sprites: listRegions(reader, undefined, "sprite", undefined, space),
|
|
293
|
+
});
|
|
131
294
|
reader?.close();
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
295
|
+
/** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
|
|
296
|
+
const idiomsResult = () => {
|
|
297
|
+
const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: "Idioms in the program" }));
|
|
298
|
+
// The macro candidates read the code regions after both imports (D73).
|
|
299
|
+
const macros = macroCandidates(space, byteAt, values["macro-batch"]);
|
|
300
|
+
// Platform pitfalls are C64 facts: drive code and banks get none (M23.7).
|
|
301
|
+
const pitfallDb = openForRead();
|
|
302
|
+
const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, listRegions(pitfallDb, undefined, "code", undefined, space)) : [];
|
|
303
|
+
pitfallDb?.close();
|
|
304
|
+
return {
|
|
305
|
+
idioms: {
|
|
306
|
+
...idioms.counts,
|
|
307
|
+
revision: imported.revision,
|
|
308
|
+
changes: { regions: imported.regions, references: imported.references },
|
|
309
|
+
conflicts: imported.conflicts.map((conflict) => showConflict(conflict, "heuristic")),
|
|
310
|
+
// Candidates are not knowledge: a larger --max-listed gives more of them.
|
|
311
|
+
candidates: {
|
|
312
|
+
dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
|
|
313
|
+
selfModified: listed(idioms.candidates.selfModified, max, formatC64Address, "a larger --max-listed"),
|
|
314
|
+
pointers: listed(pointers, max, showPointer, "a larger --max-listed"),
|
|
315
|
+
pitfalls: listed(pitfalls, max, showPitfall, "a larger --max-listed"),
|
|
316
|
+
macros: {
|
|
317
|
+
...listed(macros.candidates, max, showMacro, "a larger --max-listed, or --macro-batch <file>"),
|
|
318
|
+
...(macros.known === 0 ? {} : { known: macros.known }),
|
|
319
|
+
...(macros.leftOut === 0 ? {} : { leftOut: macros.leftOut }),
|
|
320
|
+
...(macros.batch === undefined ? {} : { batch: macros.batch }),
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
/** The coverage part of a result: its seeds, and the analyzer data that it shows to be code. */
|
|
327
|
+
const coverageResult = () => {
|
|
328
|
+
if (coverage === undefined) return {};
|
|
329
|
+
const db = openForRead();
|
|
330
|
+
const conflicts = coverageConflicts(db, coverage);
|
|
331
|
+
db?.close();
|
|
332
|
+
return {
|
|
333
|
+
coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed, romOnlyRanges: fromRom.length, romOnlyUsed: romUsed },
|
|
334
|
+
coverageConflicts: conflicts.map(({ region, executed }) => ({
|
|
335
|
+
range: { start: formatC64Address(region.start), end: formatC64Address(region.end) },
|
|
336
|
+
knowledge: { type: region.type, origin: region.origin },
|
|
337
|
+
executed,
|
|
338
|
+
problem: "coverage shows code",
|
|
339
|
+
})),
|
|
340
|
+
};
|
|
341
|
+
};
|
|
135
342
|
if (analyzer === "dxa") {
|
|
136
343
|
const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
|
|
137
344
|
const listing = attachments[0]!.toString("utf8");
|
|
138
345
|
// The listing is written only after the import: a refused import leaves no new file.
|
|
139
|
-
const imported = record(dxaFindings(result), revision,
|
|
346
|
+
const imported = record({ ...dxaFindings(result), space, ...executed }, revision, "DXA analysis of the program", runProvenance(bytes, result.toolVersion));
|
|
140
347
|
if (listingPath !== undefined) {
|
|
141
348
|
try {
|
|
142
349
|
mkdirSync(dirname(listingPath), { recursive: true });
|
|
@@ -146,25 +353,47 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
146
353
|
throw new WireFailure("operation-failed", `The script did not write the listing to ${values.listing}: ${(error as Error).message}. ${recorded}`);
|
|
147
354
|
}
|
|
148
355
|
}
|
|
356
|
+
const found = idiomsResult();
|
|
149
357
|
return {
|
|
150
358
|
analyzer: "dxa",
|
|
359
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
360
|
+
...found,
|
|
361
|
+
...coverageResult(),
|
|
151
362
|
coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
152
363
|
...imported,
|
|
153
364
|
regions: result.regions.map((region) => ({ start: formatC64Address(region.start), end: formatC64Address(region.end), classification: region.classification })),
|
|
154
|
-
labels: result.labels
|
|
155
|
-
...(result.labels.length > MAX_LISTED_FUNCTIONS ? { moreLabels: result.labels.length - MAX_LISTED_FUNCTIONS } : {}),
|
|
365
|
+
labels: listed(result.labels, max, (label) => ({ address: formatC64Address(label.address), name: label.name }), "c64-knowledge: knowledge.ts symbols"),
|
|
156
366
|
...(listingPath === undefined ? {} : { listing: values.listing }),
|
|
157
367
|
};
|
|
158
368
|
}
|
|
159
369
|
|
|
160
|
-
const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
|
|
161
|
-
|
|
370
|
+
const { result } = await analyzeWithGhidra({ imageKind, ...seeds, codeStarts, decompile }, bytes, localToolContext());
|
|
371
|
+
// Executed instruction starts that Ghidra did not decode: the instruction list and the source miss them.
|
|
372
|
+
const decoded = new Set(result.instructions.map((instruction) => instruction.address));
|
|
373
|
+
const imported = record({ ...ghidraFindings(result), space, ...executed }, revision, "Ghidra analysis of the program", runProvenance(bytes, result.toolVersion));
|
|
374
|
+
const found = idiomsResult();
|
|
375
|
+
// A start inside a dual-use region is a known overlap, for example the second instruction of a BIT skip: one list cannot hold both.
|
|
376
|
+
// Read after the idioms, which write those regions.
|
|
377
|
+
const overlapDb = coverage === undefined ? undefined : openForRead();
|
|
378
|
+
const overlaps = listRegions(overlapDb, undefined, "dual-use", undefined, space);
|
|
379
|
+
overlapDb?.close();
|
|
380
|
+
const gaps = coverage === undefined ? [] : executedStarts(coverage, inProgram).filter((address) => address >= loaded.start && address <= loaded.end && !decoded.has(address) && !overlaps.some((region) => address >= region.start && address <= region.end));
|
|
381
|
+
let instructions: { out: string; count: number } | undefined;
|
|
382
|
+
if (values.instructions !== undefined) {
|
|
383
|
+
const list = instructionList(result.instructions, coverage);
|
|
384
|
+
writeProjectFile(values.instructions, Buffer.from(`${JSON.stringify(list, null, 1)}\n`), { what: "--instructions" });
|
|
385
|
+
instructions = { out: values.instructions, count: list.length };
|
|
386
|
+
}
|
|
162
387
|
return {
|
|
163
388
|
analyzer: "ghidra",
|
|
389
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
390
|
+
...(instructions === undefined ? {} : { instructions }),
|
|
391
|
+
...found,
|
|
392
|
+
...coverageResult(),
|
|
393
|
+
...(coverage === undefined ? {} : { coverageGaps: listed(gaps, max, formatC64Address, "a larger --max-listed") }),
|
|
164
394
|
coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
165
395
|
...imported,
|
|
166
|
-
functions: result.functions
|
|
167
|
-
...(result.functions.length > MAX_LISTED_FUNCTIONS ? { moreFunctions: result.functions.length - MAX_LISTED_FUNCTIONS } : {}),
|
|
396
|
+
functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --kind routine"),
|
|
168
397
|
decompilations: result.decompilations.map((item) => ({ entry: formatC64Address(item.entry), text: item.text, ...(item.truncated ? { truncated: true } : {}) })),
|
|
169
398
|
...((result.notDecompiled ?? []).length === 0
|
|
170
399
|
? {}
|
|
@@ -172,4 +401,4 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
172
401
|
};
|
|
173
402
|
}
|
|
174
403
|
|
|
175
|
-
await runScript(() => run(
|
|
404
|
+
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Runtime evidence for static analysis (D44): the coverage as entry points,
|
|
2
|
+
// user/LLM code regions and JMP tables as entry points, and the check that
|
|
3
|
+
// shows analyzer data over executed code.
|
|
4
|
+
|
|
5
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
6
|
+
|
|
7
|
+
import { COVERAGE, coverageEntries, coverageSeedKinds, executedStarts } from "#src/c64.ts";
|
|
8
|
+
|
|
9
|
+
export { coverageEntries, coverageSeedKinds, executedStarts };
|
|
10
|
+
import { FULL_COVERAGE } from "#src/host-client/coverage.ts";
|
|
11
|
+
import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
|
|
12
|
+
import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
13
|
+
|
|
14
|
+
const FULL_MAP = FULL_COVERAGE;
|
|
15
|
+
const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
|
|
16
|
+
|
|
17
|
+
/** The byte at an address of the analyzed image, or undefined outside it. */
|
|
18
|
+
export type ImageByte = (address: number) => number | undefined;
|
|
19
|
+
|
|
20
|
+
export function imageReader(bytes: Uint8Array, kind: "prg" | "flat64k"): ImageByte {
|
|
21
|
+
if (kind === "flat64k") return (address) => bytes[address];
|
|
22
|
+
const load = bytes[0]! | (bytes[1]! << 8);
|
|
23
|
+
return (address) => (address >= load && address - load + 2 < bytes.length ? bytes[address - load + 2] : undefined);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
|
|
27
|
+
const RAM_USE = COVERAGE.ramRead | COVERAGE.ramExecute;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The image bytes under ROM that the runs never used: the CPU read or ran the
|
|
31
|
+
* ROM there, and never read or ran the RAM (R2-07). A JSR $FFE4 or an IRQ
|
|
32
|
+
* vector at $FFFE then leads into ROM, not into the leftover RAM bytes of a
|
|
33
|
+
* flat image, so the ranges are data for the analysis run.
|
|
34
|
+
*/
|
|
35
|
+
export function romOnlyRanges(map: Uint8Array, image: ImageByte): Array<{ start: number; end: number }> {
|
|
36
|
+
const ranges: Array<{ start: number; end: number }> = [];
|
|
37
|
+
for (let address = 0; address < FULL_MAP; address++) {
|
|
38
|
+
if ((map[address]! & ROM_ACCESS) === 0 || (map[address]! & RAM_USE) !== 0 || image(address) === undefined) continue;
|
|
39
|
+
const last = ranges.at(-1);
|
|
40
|
+
if (last !== undefined && last.end === address - 1) last.end = address;
|
|
41
|
+
else ranges.push({ start: address, end: address });
|
|
42
|
+
}
|
|
43
|
+
return ranges;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Entry points from semantic knowledge beyond routines: the start of each code region, and each entry of a JMP table with its target. */
|
|
47
|
+
export function codeEntries(db: DatabaseSync | undefined, image: ImageByte, space: Space = DEFAULT_SPACE): number[] {
|
|
48
|
+
const entries: number[] = [];
|
|
49
|
+
for (const region of listRegions(db, undefined, undefined, undefined, space).filter((row) => row.origin === "user" || row.origin === "llm")) {
|
|
50
|
+
if (region.type === "code") entries.push(region.start);
|
|
51
|
+
if (region.type !== "jumps") continue;
|
|
52
|
+
for (let at = region.start; at + 2 <= region.end; at += 3) {
|
|
53
|
+
entries.push(at);
|
|
54
|
+
const low = image(at + 1);
|
|
55
|
+
const high = image(at + 2);
|
|
56
|
+
if (image(at) === 0x4c && low !== undefined && high !== undefined) entries.push(low | (high << 8));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Analyzer data regions that hold executed instruction starts: the coverage shows code there. */
|
|
63
|
+
export function coverageConflicts(db: DatabaseSync | undefined, map: Uint8Array): Array<{ region: RegionRow; executed: number }> {
|
|
64
|
+
const conflicts: Array<{ region: RegionRow; executed: number }> = [];
|
|
65
|
+
for (const region of listRegions(db)) {
|
|
66
|
+
if (!ANALYZER_ORIGINS.has(region.origin) || CODE_REGION_TYPES.includes(region.type) || region.type === "dual-use") continue;
|
|
67
|
+
let executed = 0;
|
|
68
|
+
for (let address = region.start; address <= region.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) executed++;
|
|
69
|
+
if (executed > 0) conflicts.push({ region, executed });
|
|
70
|
+
}
|
|
71
|
+
return conflicts;
|
|
72
|
+
}
|