@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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.
Files changed (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: c64-static-analysis
3
+ description: Use this skill to find the structure of a C64 program without running it. DXA gives a fast first pass of code and data regions and a disassembly listing. Ghidra gives a deeper pass with routines, regions, references and decompiled code. The skill records the structure in the project knowledge and reports conflicts with the current knowledge. Do not use it for runtime questions or to name routines.
4
+ ---
5
+
6
+ # c64-static-analysis
7
+
8
+ ## Purpose
9
+
10
+ Find the static structure of a C64 program. Record the structure in the project knowledge. Report conflicts with current knowledge.
11
+
12
+ ## Inputs
13
+
14
+ - The program: a PRG file, relative to the project directory. Or 64 KiB of memory from `$0000`, for example a memory dump.
15
+ - 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
+ - Optional: routines to decompile (Ghidra), or a file for the listing (DXA).
17
+
18
+ ## Tools / execution path
19
+
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.db`.
21
+
22
+ ```
23
+ node <skill>/scripts/analyze.ts extracted/game.prg --analyzer dxa --listing analysis/game.lst
24
+ node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '$080d'
25
+ ```
26
+
27
+ | Option | Meaning |
28
+ |---|---|
29
+ | `--analyzer dxa` | Use DXA: a fast first pass. Without this option, the script uses Ghidra. |
30
+ | `--entry '$080d'` | An address where code starts. Use it again for more addresses. |
31
+ | `--listing analysis/game.lst` | DXA only: write the disassembly listing to this project file. |
32
+ | `--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
+ | `--max-listed 20` | List at most this number of labels, functions or candidates. The default is 50. Use 0 to list none. |
35
+ | `--instructions analysis/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. With `--coverage`, `executed` tells if it ran. |
36
+ | `--space drive8` | The knowledge space of the image: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
37
+ | `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` (action `read` with `out`). Use it again for more runs. The executed code becomes entry points. |
38
+
39
+ The script prints one JSON object. Addresses are `$` and four hex digits.
40
+
41
+ Both analyzers use the knowledge from the user and the LLM as seeds:
42
+
43
+ - Each routine is an entry point.
44
+ - Each region that is not code stays data.
45
+ - Each name is a label. The listing, Ghidra and the decompiler show that name.
46
+
47
+ Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
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
+ - With `--coverage`, each executed instruction that starts a flow is an entry point. A flow starts after a gap, or after RTS, RTI, JMP or BRK. Then the analyzers find code that only a vector, an RTS table or an interrupt reaches.
53
+
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.
55
+
56
+ ## Workflow
57
+
58
+ 1. Read the knowledge for the program with the c64-knowledge skill.
59
+ 2. Run a first pass with `--analyzer dxa`. For a BASIC loader, DXA finds the SYS address. Read the listing for a quick view of the code.
60
+ 3. Find the important routines. Give them names with the c64-knowledge skill.
61
+ 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
+ 5. Read the result:
63
+ - `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
64
+ - 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
+ - Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
66
+ - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
67
+ - `conflicts` lists the places where the analyzer and the current knowledge disagree.
68
+ - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`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
+ - `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 image 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.
70
+ - With `--coverage`, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
71
+ 6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
72
+
73
+ 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.
74
+
75
+ ## Knowledge
76
+
77
+ Automatic write:
78
+ - code and data regions
79
+ - names that DXA generated (labels) and routine entry points with names that Ghidra generated
80
+ - references from Ghidra: call, jump, read and write.
81
+
82
+ The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other, with one exception. With `--coverage`, a code finding replaces a DXA or Ghidra data region or data name where a run executed an instruction start. The revision keeps the coverage files as evidence. Thus run DXA first, then Ghidra with all coverage files.
83
+
84
+ 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.
85
+
86
+ A new analysis replaces the earlier facts of the same analyzer in the analyzed range. The history keeps the earlier facts. DXA labels stay until a person or the LLM removes them, because DXA names only the addresses that it sees used.
87
+
88
+ 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.
89
+
90
+ Do not store:
91
+ - the listing or decompiled code
92
+ - the function list as comments.
93
+
94
+ ## Result
95
+
96
+ The changes to the knowledge and the conflicts. For DXA, also the regions, the labels and the path of the listing. For Ghidra, also the routines and the decompiled code.
97
+
98
+ The exit status is 0 for a result, also with conflicts. The exit status is 1 for an error. The exit status is 2 for a wrong argument.
99
+
100
+ ## Failure and conflicts
101
+
102
+ - 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.
103
+ - A conflict with the problem "another address has this name": the knowledge has the generated name at a different address. The knowledge stays unchanged.
104
+ - 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 image has there. Do these steps:
105
+ 1. Find the real targets with the c64-emulator skill, or in a coverage file.
106
+ 2. If the vector bytes are also code at another time, classify them as `dual-use`.
107
+ 3. Remove the false reference with `unreference`. It does not come back.
108
+ 4. Add a `jump` reference from the JMP to each real target, and name each target as a routine.
109
+ - `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
110
+ - `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
111
+ - `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.
112
+ - `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.
113
+ - `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.
114
+
115
+ ## Handoffs
116
+
117
+ - Runtime behavior → c64-emulator
118
+ - Names and classifications → c64-knowledge
119
+ - The meaning of a C64 hardware address → c64-memory-map
120
+ - Source code for a rebuild → c64-assembler
@@ -0,0 +1,293 @@
1
+ // The c64-static-analysis script: analyzes a project program with DXA (fast
2
+ // first pass) or Ghidra (deeper), which it runs itself, seeded from
3
+ // current knowledge, and imports the structural findings into
4
+ // .c64-re-tools/knowledge.db.
5
+
6
+ import { createHash } from "node:crypto";
7
+ import { mkdirSync, writeFileSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import { parseArgs } from "node:util";
10
+
11
+ import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
12
+ import { joinCoverage } from "#src/host-client/coverage.ts";
13
+ import { WireFailure } from "#src/host-client/tools.ts";
14
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
15
+ import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
16
+ import { analyze as analyzeWithGhidra } from "#src/native/ghidra/analyze.ts";
17
+ import { localToolContext } from "#src/native/local.ts";
18
+ import { GHIDRA_LIMITS } from "#src/native/types.ts";
19
+ import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
20
+ import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
21
+ import { currentRevision, listRegions } from "#src/knowledge/read.ts";
22
+ import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
23
+ import { platformNames } from "#src/platform.ts";
24
+ import { resolveProjectPath } from "#src/project.ts";
25
+ import { runScript, UsageError } from "#src/script.ts";
26
+ import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps } from "./coverage.ts";
27
+ import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
28
+ import { findIdioms } from "./idioms.ts";
29
+ import { instructionList } from "./instructions.ts";
30
+ import { findPointers, type PointerCandidate } from "./pointers.ts";
31
+
32
+ const USAGE = `analyze.ts <image> [options]
33
+
34
+ <image> a PRG file, relative to the project directory
35
+ --analyzer <name> dxa (fast first pass) or ghidra (deeper, the default)
36
+ --flat64k the image is 64 KiB of memory from $0000, not a PRG
37
+ --entry <address> an entry point, for example '$080d' (repeatable)
38
+ --decompile <address> ghidra: a routine to decompile, at most 32 (repeatable)
39
+ --listing <file> dxa: write the disassembly listing to this project file
40
+ --instructions <file> ghidra: write every instruction (address, bytes, mnemonic,
41
+ mode, operand, target, executed) to this project file as JSON
42
+ --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): its
43
+ executed code is seeded, and analyzer data over it is reported
44
+ --max-listed <n> list at most n labels, functions or candidates (default 50,
45
+ 0 for none); the counts are always given, and knowledge has
46
+ all labels and functions
47
+ --space <space> the knowledge space of the image: c64 (the default), drive8
48
+ for 1541 drive code, or bank0 to bank255 for a cartridge bank
49
+
50
+ Routine symbols in knowledge are entry points too. Ghidra needs at least one
51
+ entry point; dxa finds the SYS of a BASIC start by itself. The result is one
52
+ JSON object. Addresses are $ and four hex digits.`;
53
+
54
+ const DEFAULT_MAX_LISTED = 50;
55
+
56
+ /** The first `max` items, the count, and where to read the rest when the list is cut. */
57
+ function listed<T, U>(items: T[], max: number, show: (item: T) => U, all: string) {
58
+ return { count: items.length, listed: items.slice(0, max).map(show), ...(items.length > max ? { rest: all } : {}) };
59
+ }
60
+
61
+ /** A pointer candidate with $ addresses, and the operand writes that would name it (D65). */
62
+ function showPointer(candidate: PointerCandidate) {
63
+ const hex = formatC64Address;
64
+ if (candidate.kind === "sprite pointer") {
65
+ 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" };
66
+ }
67
+ return { kind: candidate.kind, to: hex(candidate.to), target: hex(candidate.target), low: hex(candidate.low), high: hex(candidate.high) };
68
+ }
69
+
70
+ /** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
71
+ function withRemedy(reason: string): string {
72
+ return reason.startsWith("the Ghidra decompiler did not start")
73
+ ? `${reason} On macOS, build the native decompiler once: in <Ghidra>/support/gradle run ./gradlew buildNatives.`
74
+ : reason;
75
+ }
76
+
77
+ function addresses(values: string[] | undefined, option: string): number[] {
78
+ return (values ?? []).map((value) => {
79
+ try {
80
+ return parseC64Address(value);
81
+ } catch {
82
+ throw new UsageError(`${option} must be $ followed by four hex digits, not ${value}`);
83
+ }
84
+ });
85
+ }
86
+
87
+ /** One conflict: what the knowledge has, and what the analyzer found, under the analyzer's name. */
88
+ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<string, unknown> {
89
+ if (conflict.category === "symbol") {
90
+ return {
91
+ at: formatC64Address(conflict.address),
92
+ knowledge: { name: conflict.current.name, kind: conflict.current.kind, origin: conflict.current.origin, at: formatC64Address(conflict.current.address) },
93
+ [analyzer]: conflict.finding,
94
+ problem: conflict.reason === "kind" ? "code and data disagree" : "another address has this name",
95
+ };
96
+ }
97
+ return {
98
+ range: { start: formatC64Address(conflict.start), end: formatC64Address(conflict.end) },
99
+ knowledge: conflict.current,
100
+ [analyzer]: conflict.finding,
101
+ problem: "code and data disagree",
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Imports one analyzer's findings as one revision, refused when knowledge
107
+ * changed since the seeds were read. A refused import creates no knowledge.db.
108
+ * The revision records the image hash and the analyzer version.
109
+ */
110
+ function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
111
+ let imported: ImportResult;
112
+ try {
113
+ imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
114
+ } catch (error) {
115
+ if (error instanceof KnowledgeError && error.code === "stale-revision") {
116
+ throw new KnowledgeError("stale-revision", "The knowledge changed during the analysis. Nothing was imported. Run the analysis again.");
117
+ }
118
+ throw error;
119
+ }
120
+ return {
121
+ revision: imported.revision,
122
+ changes: { symbols: imported.symbols, regions: imported.regions, references: imported.references },
123
+ conflicts: imported.conflicts.map((conflict) => showConflict(conflict, findings.analyzer)),
124
+ };
125
+ }
126
+
127
+ async function run(argv: string[]): Promise<unknown> {
128
+ const { values, positionals } = parseArgs({
129
+ args: argv,
130
+ strict: true,
131
+ allowPositionals: true,
132
+ options: {
133
+ analyzer: { type: "string" },
134
+ flat64k: { type: "boolean" },
135
+ entry: { type: "string", multiple: true },
136
+ decompile: { type: "string", multiple: true },
137
+ listing: { type: "string" },
138
+ "max-listed": { type: "string" },
139
+ coverage: { type: "string", multiple: true },
140
+ instructions: { type: "string" },
141
+ space: { type: "string" },
142
+ help: { type: "boolean" },
143
+ },
144
+ });
145
+ if (values.help) throw new UsageError("");
146
+ if (positionals.length !== 1) throw new UsageError("give one image");
147
+ const image = positionals[0]!;
148
+ const entries = addresses(values.entry, "--entry");
149
+ const decompile = addresses(values.decompile, "--decompile");
150
+ if (decompile.length > 32) throw new UsageError("--decompile takes at most 32 routines");
151
+ const maxListed = values["max-listed"] ?? String(DEFAULT_MAX_LISTED);
152
+ if (!/^\d{1,5}$/.test(maxListed)) throw new UsageError(`--max-listed must be a whole number, not ${maxListed}`);
153
+ const max = Number(maxListed);
154
+ const analyzer = values.analyzer ?? "ghidra";
155
+ if (analyzer !== "dxa" && analyzer !== "ghidra") throw new UsageError(`--analyzer must be dxa or ghidra, not ${analyzer}`);
156
+ if (analyzer === "dxa" && decompile.length > 0) throw new UsageError("--decompile works only with --analyzer ghidra");
157
+ if (analyzer === "ghidra" && values.listing !== undefined) throw new UsageError("--listing works only with --analyzer dxa");
158
+ if (analyzer === "dxa" && values.instructions !== undefined) throw new UsageError("--instructions works only with --analyzer ghidra");
159
+ let listingPath: string | undefined;
160
+ if (values.listing !== undefined) {
161
+ try {
162
+ listingPath = resolveProjectPath(values.listing);
163
+ } catch {
164
+ throw new UsageError(`--listing must be a path relative to the project directory that stays inside it, not ${values.listing}`);
165
+ }
166
+ }
167
+
168
+ const space = values.space ?? DEFAULT_SPACE;
169
+ if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
170
+ // A coverage map is of the C64 CPU: VICE keeps none for the drive (D53).
171
+ if (space !== DEFAULT_SPACE && values.coverage !== undefined) throw new UsageError("--coverage works only in space c64");
172
+ const imageKind = values.flat64k ? "flat64k" : "prg";
173
+ const bytes = readProjectFile(image).bytes;
174
+ const runs = readCoverageMaps(values.coverage ?? []);
175
+ const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
176
+ const byteAt = imageReader(bytes, imageKind);
177
+ // With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58), and the files are the evidence.
178
+ const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
179
+ const coverageEvidence = coverage === undefined ? {} : { evidence: (values.coverage ?? []).map((path) => ({ kind: "coverage" as const, ref: path, sha256: createHash("sha256").update(readProjectFile(path).bytes).digest("hex") })) };
180
+
181
+ let loaded: { start: number; end: number };
182
+ try {
183
+ loaded = imageKind === "flat64k" ? { start: 0x0000, end: bytes.length - 1 } : prgRange(bytes);
184
+ } catch (error) {
185
+ throw new UsageError(`${image}: ${(error as Error).message}`);
186
+ }
187
+ const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, runs);
188
+
189
+ const reader = openForRead();
190
+ const revision = currentRevision(reader);
191
+ const seeds = seedsFromKnowledge(reader, entries, space);
192
+ // Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound (D44).
193
+ const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, byteAt);
194
+ const ordered = [...new Set([...seeds.entryPoints, ...codeEntries(reader, byteAt, space), ...idioms.entryPoints, ...fromCoverage])];
195
+ seeds.entryPoints = ordered.slice(0, GHIDRA_LIMITS.entryPoints).sort((a, b) => a - b);
196
+ // Idiom tables are data for this run too; semantic data ranges come first.
197
+ seeds.dataRanges = [...seeds.dataRanges, ...idioms.dataRanges.filter((range) => !seeds.dataRanges.some((seed) => range.start <= seed.end && range.end >= seed.start))].slice(0, GHIDRA_LIMITS.dataRanges);
198
+ const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry)).length;
199
+ // Platform names are C64 names: drive code and banks get none.
200
+ const platform = space === DEFAULT_SPACE ? platformNames() : [];
201
+ const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, {
202
+ platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
203
+ sprites: listRegions(reader, undefined, "sprite", undefined, space),
204
+ });
205
+ reader?.close();
206
+ /** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
207
+ const idiomsResult = () => {
208
+ const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: `Idioms in ${image}` }));
209
+ return {
210
+ idioms: {
211
+ ...idioms.counts,
212
+ revision: imported.revision,
213
+ changes: { regions: imported.regions, references: imported.references },
214
+ conflicts: imported.conflicts.map((conflict) => showConflict(conflict, "heuristic")),
215
+ // Candidates are not knowledge: a larger --max-listed gives more of them.
216
+ candidates: {
217
+ dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
218
+ selfModified: listed(idioms.candidates.selfModified, max, formatC64Address, "a larger --max-listed"),
219
+ pointers: listed(pointers, max, showPointer, "a larger --max-listed"),
220
+ },
221
+ },
222
+ };
223
+ };
224
+ /** The coverage part of a result: its seeds, and the analyzer data that it shows to be code. */
225
+ const coverageResult = () => {
226
+ if (coverage === undefined) return {};
227
+ const db = openForRead();
228
+ const conflicts = coverageConflicts(db, coverage);
229
+ db?.close();
230
+ return {
231
+ coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed },
232
+ coverageConflicts: conflicts.map(({ region, executed }) => ({
233
+ range: { start: formatC64Address(region.start), end: formatC64Address(region.end) },
234
+ knowledge: { type: region.type, origin: region.origin },
235
+ executed,
236
+ problem: "coverage shows code",
237
+ })),
238
+ };
239
+ };
240
+ if (analyzer === "dxa") {
241
+ const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
242
+ const listing = attachments[0]!.toString("utf8");
243
+ // The listing is written only after the import: a refused import leaves no new file.
244
+ const imported = record({ ...dxaFindings(result), space, ...executed }, revision, `DXA analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
245
+ if (listingPath !== undefined) {
246
+ try {
247
+ mkdirSync(dirname(listingPath), { recursive: true });
248
+ writeFileSync(listingPath, listing);
249
+ } catch (error) {
250
+ const recorded = imported.revision === null ? "The knowledge did not change." : `The knowledge has the findings as revision ${imported.revision}.`;
251
+ throw new WireFailure("operation-failed", `The script did not write the listing to ${values.listing}: ${(error as Error).message}. ${recorded}`);
252
+ }
253
+ }
254
+ const found = idiomsResult();
255
+ return {
256
+ analyzer: "dxa",
257
+ ...(space === DEFAULT_SPACE ? {} : { space }),
258
+ ...found,
259
+ ...coverageResult(),
260
+ coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
261
+ ...imported,
262
+ regions: result.regions.map((region) => ({ start: formatC64Address(region.start), end: formatC64Address(region.end), classification: region.classification })),
263
+ labels: listed(result.labels, max, (label) => ({ address: formatC64Address(label.address), name: label.name }), "c64-knowledge: knowledge.ts symbols"),
264
+ ...(listingPath === undefined ? {} : { listing: values.listing }),
265
+ };
266
+ }
267
+
268
+ const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
269
+ const imported = record({ ...ghidraFindings(result), space, ...executed }, revision, `Ghidra analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
270
+ const found = idiomsResult();
271
+ let instructions: { out: string; count: number } | undefined;
272
+ if (values.instructions !== undefined) {
273
+ const list = instructionList(result.instructions, coverage);
274
+ writeProjectFile(values.instructions, Buffer.from(`${JSON.stringify(list, null, 1)}\n`), { what: "--instructions" });
275
+ instructions = { out: values.instructions, count: list.length };
276
+ }
277
+ return {
278
+ analyzer: "ghidra",
279
+ ...(space === DEFAULT_SPACE ? {} : { space }),
280
+ ...(instructions === undefined ? {} : { instructions }),
281
+ ...found,
282
+ ...coverageResult(),
283
+ coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
284
+ ...imported,
285
+ functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --kind routine"),
286
+ decompilations: result.decompilations.map((item) => ({ entry: formatC64Address(item.entry), text: item.text, ...(item.truncated ? { truncated: true } : {}) })),
287
+ ...((result.notDecompiled ?? []).length === 0
288
+ ? {}
289
+ : { notDecompiled: result.notDecompiled!.map((item) => ({ entry: formatC64Address(item.entry), reason: withRemedy(item.reason) })) }),
290
+ };
291
+ }
292
+
293
+ await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });
@@ -0,0 +1,78 @@
1
+ // Runtime evidence for static analysis (D44): coverage files 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, instructionLength } from "#src/c64.ts";
8
+ import { FULL_COVERAGE, readCoverageMaps as readMaps } from "#src/host-client/coverage.ts";
9
+ import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
10
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
11
+ import { UsageError } from "#src/script.ts";
12
+
13
+ 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
+ const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
17
+
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
+ /** The byte at an address of the analyzed image, or undefined outside it. */
26
+ export type ImageByte = (address: number) => number | undefined;
27
+
28
+ export function imageReader(bytes: Uint8Array, kind: "prg" | "flat64k"): ImageByte {
29
+ if (kind === "flat64k") return (address) => bytes[address];
30
+ const load = bytes[0]! | (bytes[1]! << 8);
31
+ return (address) => (address >= load && address - load + 2 < bytes.length ? bytes[address - load + 2] : undefined);
32
+ }
33
+
34
+ /**
35
+ * The executed instruction starts in RAM that begin a flow: the first after
36
+ * a gap, or the first after an instruction that does not fall through. The
37
+ * analyzers follow the rest of a flow themselves.
38
+ */
39
+ export function coverageEntries(map: Uint8Array, image: ImageByte): number[] {
40
+ const entries: number[] = [];
41
+ let falls: number | undefined;
42
+ for (let address = 0; address < FULL_MAP; address++) {
43
+ if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
44
+ const opcode = image(address);
45
+ if (opcode === undefined) continue;
46
+ if (falls !== address) entries.push(address);
47
+ falls = FLOW_ENDS.has(opcode) ? undefined : address + instructionLength(opcode);
48
+ }
49
+ return entries;
50
+ }
51
+
52
+ /** Entry points from semantic knowledge beyond routines: the start of each code region, and each entry of a JMP table with its target. */
53
+ export function codeEntries(db: DatabaseSync | undefined, image: ImageByte, space: Space = DEFAULT_SPACE): number[] {
54
+ const entries: number[] = [];
55
+ for (const region of listRegions(db, undefined, undefined, undefined, space).filter((row) => row.origin === "user" || row.origin === "llm")) {
56
+ if (region.type === "code") entries.push(region.start);
57
+ if (region.type !== "jumps") continue;
58
+ for (let at = region.start; at + 2 <= region.end; at += 3) {
59
+ entries.push(at);
60
+ const low = image(at + 1);
61
+ const high = image(at + 2);
62
+ if (image(at) === 0x4c && low !== undefined && high !== undefined) entries.push(low | (high << 8));
63
+ }
64
+ }
65
+ return entries;
66
+ }
67
+
68
+ /** Analyzer data regions that hold executed instruction starts: the coverage shows code there. */
69
+ export function coverageConflicts(db: DatabaseSync | undefined, map: Uint8Array): Array<{ region: RegionRow; executed: number }> {
70
+ const conflicts: Array<{ region: RegionRow; executed: number }> = [];
71
+ for (const region of listRegions(db)) {
72
+ if (!ANALYZER_ORIGINS.has(region.origin) || CODE_REGION_TYPES.includes(region.type) || region.type === "dual-use") continue;
73
+ let executed = 0;
74
+ for (let address = region.start; address <= region.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) executed++;
75
+ if (executed > 0) conflicts.push({ region, executed });
76
+ }
77
+ return conflicts;
78
+ }
@@ -0,0 +1,75 @@
1
+ // Knowledge seeds for an analyzer, and the mapping of a Ghidra result to the
2
+ // common normalized findings.
3
+
4
+ import type { DatabaseSync } from "node:sqlite";
5
+
6
+ import type { NormalizedFindings } from "#src/knowledge/import.ts";
7
+ import { listRegions, listSymbols } from "#src/knowledge/read.ts";
8
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
9
+ import type { DxaResult, GhidraParams, GhidraResult } from "#src/native/types.ts";
10
+
11
+ export type Seeds = Pick<GhidraParams, "entryPoints" | "dataRanges" | "labels">;
12
+
13
+ const semantic = (row: { origin: string }) => row.origin === "user" || row.origin === "llm";
14
+
15
+ /**
16
+ * Seeds from current semantic knowledge: routines from the user or
17
+ * the LLM are entry points, their non-code regions are data, and their names
18
+ * are labels. Analyzer facts are never seeds: fed back, an analyzer's own
19
+ * earlier guess would force the same answer again, and a re-analysis could
20
+ * not retire it.
21
+ */
22
+ export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = [], space: Space = DEFAULT_SPACE): Seeds {
23
+ const symbols = listSymbols(db, undefined, undefined, undefined, space).filter(semantic);
24
+ const entryPoints = [...new Set([...extraEntryPoints, ...symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address)])].sort((a, b) => a - b);
25
+ return {
26
+ entryPoints,
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 (D42).
29
+ .filter((region) => semantic(region) && !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
30
+ .map((region) => ({ start: region.start, end: region.end })),
31
+ labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Ghidra's result as normalized findings. A function named by an echoed seed
37
+ * stays in the snapshot: its address holds the seed's semantic symbol, which
38
+ * the importer never replaces, so the name keeps its owner,
39
+ * and a code/data disagreement with that symbol is still reported.
40
+ */
41
+ export function ghidraFindings(result: GhidraResult): NormalizedFindings {
42
+ // Ghidra can give one generated name to two functions, for example
43
+ // thunk_FUN_e434 to two thunks of one routine. A name
44
+ // must have one address, so a repeated name that is not a seed gets its address.
45
+ const uses = new Map<string, number>();
46
+ for (const fn of result.functions) uses.set(fn.name, (uses.get(fn.name) ?? 0) + 1);
47
+ const nameOf = (fn: GhidraResult["functions"][number]) =>
48
+ fn.nameSource !== "seed" && uses.get(fn.name)! > 1 ? `${fn.name}_${fn.entry.toString(16).padStart(4, "0")}` : fn.name;
49
+ return {
50
+ analyzer: "ghidra",
51
+ coverage: result.coverage,
52
+ 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: "routine" as const })),
54
+ regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
55
+ references: result.references.map((reference) => ({ from: reference.from, to: reference.to, kind: reference.type })),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * DXA's result as normalized findings. A label in one of DXA's data
61
+ * regions becomes a symbol of kind data, any other label one of kind label.
62
+ * DXA names only referenced addresses, so labels are not authoritative, while
63
+ * its regions cover every byte and are.
64
+ */
65
+ export function dxaFindings(result: DxaResult): NormalizedFindings {
66
+ const inData = (address: number) => result.regions.some((region) => region.classification === "data" && region.start <= address && address <= region.end);
67
+ return {
68
+ analyzer: "dxa",
69
+ coverage: result.coverage,
70
+ authoritative: { symbols: result.completeness.labels, regions: result.completeness.regions, references: false },
71
+ symbols: result.labels.map((label) => ({ address: label.address, name: label.name, kind: inData(label.address) ? ("data" as const) : ("label" as const) })),
72
+ regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
73
+ references: [],
74
+ };
75
+ }