@henols/c64-re-tools 2.0.0-rc.8 → 2.0.0
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 +98 -115
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- 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 +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -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 +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- 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 +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- 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} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +43 -8
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- 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 +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/src/trace.ts +55 -23
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
|
@@ -11,30 +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
|
-
| `--flat64k` | The file is 64 KiB of memory from `$0000`, not a PRG. |
|
|
34
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
|
|
36
|
-
| `--space drive8` | The knowledge space of the
|
|
37
|
-
| `--
|
|
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. |
|
|
38
38
|
|
|
39
39
|
The script prints one JSON object. Addresses are `$` and four hex digits.
|
|
40
40
|
|
|
@@ -49,9 +49,21 @@ Facts that an analyzer found are not seeds. A new analysis can then correct or r
|
|
|
49
49
|
More seeds come from evidence:
|
|
50
50
|
|
|
51
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
|
-
-
|
|
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.
|
|
53
56
|
|
|
54
|
-
The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used.
|
|
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`.
|
|
55
67
|
|
|
56
68
|
## Workflow
|
|
57
69
|
|
|
@@ -60,13 +72,16 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
60
72
|
3. Find the important routines. Give them names with the c64-knowledge skill.
|
|
61
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.
|
|
62
74
|
5. Read the result:
|
|
63
|
-
- `changes` counts the
|
|
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.
|
|
64
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`).
|
|
65
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.
|
|
66
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`.
|
|
67
79
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
68
|
-
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
69
|
-
-
|
|
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.
|
|
70
85
|
6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
|
|
71
86
|
|
|
72
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.
|
|
@@ -75,14 +90,16 @@ The script records only structure: generated names, code and data regions, and r
|
|
|
75
90
|
|
|
76
91
|
Automatic write:
|
|
77
92
|
- code and data regions
|
|
78
|
-
- names that DXA generated
|
|
93
|
+
- analyzer names: the labels that DXA generated and the functions that Ghidra found, for example `l17e2` and `FUN_17e2`
|
|
79
94
|
- references from Ghidra: call, jump, read and write.
|
|
80
95
|
|
|
81
|
-
|
|
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.
|
|
82
99
|
|
|
83
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.
|
|
84
101
|
|
|
85
|
-
A new analysis replaces the earlier facts of the same analyzer in the analyzed range. The history keeps the earlier facts.
|
|
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.
|
|
86
103
|
|
|
87
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.
|
|
88
105
|
|
|
@@ -100,7 +117,7 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
|
|
|
100
117
|
|
|
101
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.
|
|
102
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.
|
|
103
|
-
- 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
|
|
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:
|
|
104
121
|
1. Find the real targets with the c64-emulator skill, or in a coverage file.
|
|
105
122
|
2. If the vector bytes are also code at another time, classify them as `dual-use`.
|
|
106
123
|
3. Remove the false reference with `unreference`. It does not come back.
|
|
@@ -1,49 +1,54 @@
|
|
|
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
|
|
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
|
-
import { createHash } from "node:crypto";
|
|
7
6
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
7
|
import { dirname } from "node:path";
|
|
9
8
|
import { parseArgs } from "node:util";
|
|
10
9
|
|
|
11
10
|
import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
|
|
12
|
-
import { joinCoverage } from "#src/host-client/coverage.ts";
|
|
13
11
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
14
|
-
import {
|
|
12
|
+
import { writeProjectFile } from "#src/host-client/transfer.ts";
|
|
15
13
|
import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
|
|
16
14
|
import { analyze as analyzeWithGhidra } from "#src/native/ghidra/analyze.ts";
|
|
17
15
|
import { localToolContext } from "#src/native/local.ts";
|
|
18
16
|
import { GHIDRA_LIMITS } from "#src/native/types.ts";
|
|
19
17
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
18
|
+
import { analyzedBytes, programCoverage, requireProgram } from "#src/knowledge/program.ts";
|
|
20
19
|
import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
|
|
21
|
-
import { currentRevision } from "#src/knowledge/read.ts";
|
|
20
|
+
import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
22
21
|
import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
|
|
22
|
+
import { platformNames } from "#src/platform.ts";
|
|
23
23
|
import { resolveProjectPath } from "#src/project.ts";
|
|
24
24
|
import { runScript, UsageError } from "#src/script.ts";
|
|
25
|
-
import { codeEntries, coverageConflicts, coverageEntries, imageReader,
|
|
26
|
-
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
|
|
27
|
-
import { findIdioms } from "./idioms.ts";
|
|
28
|
-
import { instructionList } from "./instructions.ts";
|
|
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";
|
|
29
32
|
|
|
30
|
-
const USAGE = `analyze.ts
|
|
33
|
+
const USAGE = `analyze.ts [options]
|
|
34
|
+
|
|
35
|
+
The program and the coverage come from the knowledge: 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.
|
|
31
38
|
|
|
32
|
-
<image> a PRG file, relative to the project directory
|
|
33
39
|
--analyzer <name> dxa (fast first pass) or ghidra (deeper, the default)
|
|
34
|
-
--flat64k the image is 64 KiB of memory from $0000, not a PRG
|
|
35
40
|
--entry <address> an entry point, for example '$080d' (repeatable)
|
|
36
41
|
--decompile <address> ghidra: a routine to decompile, at most 32 (repeatable)
|
|
37
42
|
--listing <file> dxa: write the disassembly listing to this project file
|
|
38
43
|
--instructions <file> ghidra: write every instruction (address, bytes, mnemonic,
|
|
39
44
|
mode, operand, target, executed) to this project file as JSON
|
|
40
|
-
--coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): its
|
|
41
|
-
executed code is seeded, and analyzer data over it is reported
|
|
42
45
|
--max-listed <n> list at most n labels, functions or candidates (default 50,
|
|
43
46
|
0 for none); the counts are always given, and knowledge has
|
|
44
47
|
all labels and functions
|
|
45
|
-
--space <space> the knowledge space of the
|
|
48
|
+
--space <space> the knowledge space of the program: c64 (the default), drive8
|
|
46
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
|
|
47
52
|
|
|
48
53
|
Routine symbols in knowledge are entry points too. Ghidra needs at least one
|
|
49
54
|
entry point; dxa finds the SYS of a BASIC start by itself. The result is one
|
|
@@ -56,6 +61,92 @@ function listed<T, U>(items: T[], max: number, show: (item: T) => U, all: string
|
|
|
56
61
|
return { count: items.length, listed: items.slice(0, max).map(show), ...(items.length > max ? { rest: all } : {}) };
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/** A pitfall with $ addresses and what goes wrong. */
|
|
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. */
|
|
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. */
|
|
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 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
|
+
}
|
|
149
|
+
|
|
59
150
|
/** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
|
|
60
151
|
function withRemedy(reason: string): string {
|
|
61
152
|
return reason.startsWith("the Ghidra decompiler did not start")
|
|
@@ -93,10 +184,10 @@ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<stri
|
|
|
93
184
|
|
|
94
185
|
/**
|
|
95
186
|
* Imports one analyzer's findings as one revision, refused when knowledge
|
|
96
|
-
* 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.
|
|
97
188
|
* The revision records the image hash and the analyzer version.
|
|
98
189
|
*/
|
|
99
|
-
function record(findings: NormalizedFindings, revision:
|
|
190
|
+
function record(findings: NormalizedFindings, revision: string, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
|
|
100
191
|
let imported: ImportResult;
|
|
101
192
|
try {
|
|
102
193
|
imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
|
|
@@ -108,7 +199,7 @@ function record(findings: NormalizedFindings, revision: number, description: str
|
|
|
108
199
|
}
|
|
109
200
|
return {
|
|
110
201
|
revision: imported.revision,
|
|
111
|
-
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 },
|
|
112
203
|
conflicts: imported.conflicts.map((conflict) => showConflict(conflict, findings.analyzer)),
|
|
113
204
|
};
|
|
114
205
|
}
|
|
@@ -120,20 +211,18 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
120
211
|
allowPositionals: true,
|
|
121
212
|
options: {
|
|
122
213
|
analyzer: { type: "string" },
|
|
123
|
-
flat64k: { type: "boolean" },
|
|
124
214
|
entry: { type: "string", multiple: true },
|
|
125
215
|
decompile: { type: "string", multiple: true },
|
|
126
216
|
listing: { type: "string" },
|
|
127
217
|
"max-listed": { type: "string" },
|
|
128
|
-
coverage: { type: "string", multiple: true },
|
|
129
218
|
instructions: { type: "string" },
|
|
130
219
|
space: { type: "string" },
|
|
220
|
+
"macro-batch": { type: "string" },
|
|
131
221
|
help: { type: "boolean" },
|
|
132
222
|
},
|
|
133
223
|
});
|
|
134
224
|
if (values.help) throw new UsageError("");
|
|
135
|
-
if (positionals.length !==
|
|
136
|
-
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)");
|
|
137
226
|
const entries = addresses(values.entry, "--entry");
|
|
138
227
|
const decompile = addresses(values.decompile, "--decompile");
|
|
139
228
|
if (decompile.length > 32) throw new UsageError("--decompile takes at most 32 routines");
|
|
@@ -150,45 +239,68 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
150
239
|
try {
|
|
151
240
|
listingPath = resolveProjectPath(values.listing);
|
|
152
241
|
} catch {
|
|
153
|
-
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"]}`);
|
|
154
251
|
}
|
|
155
252
|
}
|
|
156
253
|
|
|
157
254
|
const space = values.space ?? DEFAULT_SPACE;
|
|
158
255
|
if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
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.
|
|
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.
|
|
165
263
|
const byteAt = imageReader(bytes, imageKind);
|
|
166
|
-
|
|
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.
|
|
167
266
|
const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
let loaded: { start: number; end: number };
|
|
171
|
-
try {
|
|
172
|
-
loaded = imageKind === "flat64k" ? { start: 0x0000, end: bytes.length - 1 } : prgRange(bytes);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
throw new UsageError(`${image}: ${(error as Error).message}`);
|
|
175
|
-
}
|
|
176
|
-
const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, runs);
|
|
267
|
+
const loaded = imageKind === "flat64k" ? { start: 0x0000, end: 0xffff } : prgRange(bytes);
|
|
268
|
+
const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, covered);
|
|
177
269
|
|
|
178
|
-
const reader = openForRead();
|
|
179
270
|
const revision = currentRevision(reader);
|
|
180
271
|
const seeds = seedsFromKnowledge(reader, entries, space);
|
|
181
|
-
// Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound
|
|
182
|
-
const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage,
|
|
183
|
-
const
|
|
272
|
+
// Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound.
|
|
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; 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])];
|
|
184
278
|
seeds.entryPoints = ordered.slice(0, GHIDRA_LIMITS.entryPoints).sort((a, b) => a - b);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
});
|
|
188
294
|
reader?.close();
|
|
189
295
|
/** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
|
|
190
296
|
const idiomsResult = () => {
|
|
191
|
-
const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description:
|
|
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.
|
|
299
|
+
const macros = macroCandidates(space, byteAt, values["macro-batch"]);
|
|
300
|
+
// Platform pitfalls are C64 facts: drive code and banks get none.
|
|
301
|
+
const pitfallDb = openForRead();
|
|
302
|
+
const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, listRegions(pitfallDb, undefined, "code", undefined, space)) : [];
|
|
303
|
+
pitfallDb?.close();
|
|
192
304
|
return {
|
|
193
305
|
idioms: {
|
|
194
306
|
...idioms.counts,
|
|
@@ -199,6 +311,14 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
199
311
|
candidates: {
|
|
200
312
|
dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
|
|
201
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
|
+
},
|
|
202
322
|
},
|
|
203
323
|
},
|
|
204
324
|
};
|
|
@@ -210,7 +330,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
210
330
|
const conflicts = coverageConflicts(db, coverage);
|
|
211
331
|
db?.close();
|
|
212
332
|
return {
|
|
213
|
-
coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed },
|
|
333
|
+
coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed, romOnlyRanges: fromRom.length, romOnlyUsed: romUsed },
|
|
214
334
|
coverageConflicts: conflicts.map(({ region, executed }) => ({
|
|
215
335
|
range: { start: formatC64Address(region.start), end: formatC64Address(region.end) },
|
|
216
336
|
knowledge: { type: region.type, origin: region.origin },
|
|
@@ -223,7 +343,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
223
343
|
const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
|
|
224
344
|
const listing = attachments[0]!.toString("utf8");
|
|
225
345
|
// The listing is written only after the import: a refused import leaves no new file.
|
|
226
|
-
const imported = record({ ...dxaFindings(result), space, ...executed }, revision,
|
|
346
|
+
const imported = record({ ...dxaFindings(result), space, ...executed }, revision, "DXA analysis of the program", runProvenance(bytes, result.toolVersion));
|
|
227
347
|
if (listingPath !== undefined) {
|
|
228
348
|
try {
|
|
229
349
|
mkdirSync(dirname(listingPath), { recursive: true });
|
|
@@ -247,9 +367,17 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
247
367
|
};
|
|
248
368
|
}
|
|
249
369
|
|
|
250
|
-
const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
|
|
251
|
-
|
|
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));
|
|
252
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));
|
|
253
381
|
let instructions: { out: string; count: number } | undefined;
|
|
254
382
|
if (values.instructions !== undefined) {
|
|
255
383
|
const list = instructionList(result.instructions, coverage);
|
|
@@ -262,6 +390,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
262
390
|
...(instructions === undefined ? {} : { instructions }),
|
|
263
391
|
...found,
|
|
264
392
|
...coverageResult(),
|
|
393
|
+
...(coverage === undefined ? {} : { coverageGaps: listed(gaps, max, formatC64Address, "a larger --max-listed") }),
|
|
265
394
|
coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
266
395
|
...imported,
|
|
267
396
|
functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --kind routine"),
|
|
@@ -272,4 +401,4 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
272
401
|
};
|
|
273
402
|
}
|
|
274
403
|
|
|
275
|
-
await runScript(() => run(
|
|
404
|
+
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
|
|
@@ -1,27 +1,19 @@
|
|
|
1
|
-
// Runtime evidence for static analysis
|
|
1
|
+
// Runtime evidence for static analysis: the coverage as entry points,
|
|
2
2
|
// user/LLM code regions and JMP tables as entry points, and the check that
|
|
3
3
|
// shows analyzer data over executed code.
|
|
4
4
|
|
|
5
5
|
import type { DatabaseSync } from "node:sqlite";
|
|
6
6
|
|
|
7
|
-
import { COVERAGE,
|
|
8
|
-
|
|
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";
|
|
9
11
|
import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
|
|
10
12
|
import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
11
|
-
import { UsageError } from "#src/script.ts";
|
|
12
13
|
|
|
13
14
|
const FULL_MAP = FULL_COVERAGE;
|
|
14
|
-
/** Instructions after which the next instruction is not reached by falling through. */
|
|
15
|
-
const FLOW_ENDS = new Set([0x00, 0x40, 0x4c, 0x60, 0x6c]);
|
|
16
15
|
const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
|
|
17
16
|
|
|
18
|
-
/** Reads coverage files, one map per file (a run). */
|
|
19
|
-
export function readCoverageMaps(paths: string[]): Uint8Array[] {
|
|
20
|
-
return readMaps(paths, (message) => {
|
|
21
|
-
throw new UsageError(message);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
/** The byte at an address of the analyzed image, or undefined outside it. */
|
|
26
18
|
export type ImageByte = (address: number) => number | undefined;
|
|
27
19
|
|
|
@@ -31,22 +23,24 @@ export function imageReader(bytes: Uint8Array, kind: "prg" | "flat64k"): ImageBy
|
|
|
31
23
|
return (address) => (address >= load && address - load + 2 < bytes.length ? bytes[address - load + 2] : undefined);
|
|
32
24
|
}
|
|
33
25
|
|
|
26
|
+
const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
|
|
27
|
+
const RAM_USE = COVERAGE.ramRead | COVERAGE.ramExecute;
|
|
28
|
+
|
|
34
29
|
/**
|
|
35
|
-
* The
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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. 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.
|
|
38
34
|
*/
|
|
39
|
-
export function
|
|
40
|
-
const
|
|
41
|
-
let falls: number | undefined;
|
|
35
|
+
export function romOnlyRanges(map: Uint8Array, image: ImageByte): Array<{ start: number; end: number }> {
|
|
36
|
+
const ranges: Array<{ start: number; end: number }> = [];
|
|
42
37
|
for (let address = 0; address < FULL_MAP; address++) {
|
|
43
|
-
if ((map[address]! &
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
falls = FLOW_ENDS.has(opcode) ? undefined : address + instructionLength(opcode);
|
|
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 });
|
|
48
42
|
}
|
|
49
|
-
return
|
|
43
|
+
return ranges;
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
/** Entry points from semantic knowledge beyond routines: the start of each code region, and each entry of a JMP table with its target. */
|
|
@@ -25,7 +25,7 @@ export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoint
|
|
|
25
25
|
return {
|
|
26
26
|
entryPoints,
|
|
27
27
|
dataRanges: listRegions(db, undefined, undefined, undefined, space)
|
|
28
|
-
// A JMP table is code and dual-use bytes are both: neither is a data range
|
|
28
|
+
// A JMP table is code and dual-use bytes are both: neither is a data range.
|
|
29
29
|
.filter((region) => semantic(region) && !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
|
|
30
30
|
.map((region) => ({ start: region.start, end: region.end })),
|
|
31
31
|
labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
|
|
@@ -46,11 +46,15 @@ export function ghidraFindings(result: GhidraResult): NormalizedFindings {
|
|
|
46
46
|
for (const fn of result.functions) uses.set(fn.name, (uses.get(fn.name) ?? 0) + 1);
|
|
47
47
|
const nameOf = (fn: GhidraResult["functions"][number]) =>
|
|
48
48
|
fn.nameSource !== "seed" && uses.get(fn.name)! > 1 ? `${fn.name}_${fn.entry.toString(16).padStart(4, "0")}` : fn.name;
|
|
49
|
+
// A function that branches and jumps reach, and no call, is a place in a routine: a label.
|
|
50
|
+
const called = new Set(result.references.filter((reference) => reference.type === "call").map((reference) => reference.to));
|
|
51
|
+
const jumped = new Set(result.references.filter((reference) => reference.type === "jump").map((reference) => reference.to));
|
|
52
|
+
const kindOf = (fn: GhidraResult["functions"][number]) => (jumped.has(fn.entry) && !called.has(fn.entry) ? ("label" as const) : ("routine" as const));
|
|
49
53
|
return {
|
|
50
54
|
analyzer: "ghidra",
|
|
51
55
|
coverage: result.coverage,
|
|
52
56
|
authoritative: { symbols: result.completeness.functions, regions: result.completeness.regions, references: result.completeness.references },
|
|
53
|
-
symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind:
|
|
57
|
+
symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind: kindOf(fn) })),
|
|
54
58
|
regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
|
|
55
59
|
references: result.references.map((reference) => ({ from: reference.from, to: reference.to, kind: reference.type })),
|
|
56
60
|
};
|