dasmos 0.1.2__py3-none-any.whl
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.
- dasmos/__init__.py +65 -0
- dasmos/_text.py +57 -0
- dasmos/cli.py +485 -0
- dasmos/core/__init__.py +8 -0
- dasmos/core/annotations.py +207 -0
- dasmos/core/classification.py +223 -0
- dasmos/core/config.py +74 -0
- dasmos/core/cpu_state.py +46 -0
- dasmos/core/disassembly.py +161 -0
- dasmos/core/labels.py +373 -0
- dasmos/core/markdown_asm.py +374 -0
- dasmos/core/memory.py +318 -0
- dasmos/core/move.py +301 -0
- dasmos/cpu.py +388 -0
- dasmos/disassembler.py +1350 -0
- dasmos/environment.py +141 -0
- dasmos/exceptions.py +6 -0
- dasmos/ext/__init__.py +8 -0
- dasmos/ext/cpus/__init__.py +10 -0
- dasmos/ext/cpus/cmos65c02/__init__.py +22 -0
- dasmos/ext/cpus/cmos65c02/cpu.py +148 -0
- dasmos/ext/cpus/nmos6502/__init__.py +22 -0
- dasmos/ext/cpus/nmos6502/cpu.py +550 -0
- dasmos/ext/environments/__init__.py +0 -0
- dasmos/ext/environments/acorn_bbc_hardware/__init__.py +13 -0
- dasmos/ext/environments/acorn_bbc_hardware/environment.py +164 -0
- dasmos/ext/environments/acorn_mos/__init__.py +13 -0
- dasmos/ext/environments/acorn_mos/enums.py +299 -0
- dasmos/ext/environments/acorn_mos/environment.py +182 -0
- dasmos/ext/environments/acorn_mos/hooks.py +212 -0
- dasmos/ext/environments/acorn_sideways_rom/__init__.py +13 -0
- dasmos/ext/environments/acorn_sideways_rom/environment.py +183 -0
- dasmos/ext/renderers/__init__.py +10 -0
- dasmos/ext/renderers/beebasm/__init__.py +16 -0
- dasmos/ext/renderers/beebasm/renderer.py +1743 -0
- dasmos/ext/renderers/json/__init__.py +16 -0
- dasmos/ext/renderers/json/renderer.py +718 -0
- dasmos/extension.py +250 -0
- dasmos/hooks.py +76 -0
- dasmos/ir.py +117 -0
- dasmos/output.py +82 -0
- dasmos/py.typed +0 -0
- dasmos/renderer.py +272 -0
- dasmos-0.1.2.dist-info/METADATA +315 -0
- dasmos-0.1.2.dist-info/RECORD +49 -0
- dasmos-0.1.2.dist-info/WHEEL +5 -0
- dasmos-0.1.2.dist-info/entry_points.txt +15 -0
- dasmos-0.1.2.dist-info/licenses/LICENSE +21 -0
- dasmos-0.1.2.dist-info/top_level.txt +1 -0
dasmos/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""dasmos — a pluggable tracing disassembler.
|
|
2
|
+
|
|
3
|
+
The top-level package re-exports the public API. Consumers should import
|
|
4
|
+
from :mod:`dasmos` rather than reaching into sub-modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.2"
|
|
8
|
+
|
|
9
|
+
from dasmos.core.annotations import Align, Annotation, AnnotationStore, Banner, Comment
|
|
10
|
+
from dasmos.cpu import (
|
|
11
|
+
Cpu,
|
|
12
|
+
CpuExtensionError,
|
|
13
|
+
FlowControl,
|
|
14
|
+
Opcode,
|
|
15
|
+
OperandKind,
|
|
16
|
+
cpu_names,
|
|
17
|
+
create_cpu,
|
|
18
|
+
describe_cpu,
|
|
19
|
+
)
|
|
20
|
+
from dasmos.disassembler import Disassembler, DisassemblerError
|
|
21
|
+
from dasmos.exceptions import DasmosError
|
|
22
|
+
from dasmos.extension import Extension, ExtensionError
|
|
23
|
+
from dasmos.ir import IntermediateRepresentation
|
|
24
|
+
from dasmos.output import Output, StructuredOutput, TextOutput
|
|
25
|
+
from dasmos.renderer import (
|
|
26
|
+
Renderer,
|
|
27
|
+
RendererExtensionError,
|
|
28
|
+
StructuredRenderer,
|
|
29
|
+
TextRenderer,
|
|
30
|
+
create_renderer,
|
|
31
|
+
describe_renderer,
|
|
32
|
+
renderer_names,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Align",
|
|
37
|
+
"Annotation",
|
|
38
|
+
"AnnotationStore",
|
|
39
|
+
"Banner",
|
|
40
|
+
"Comment",
|
|
41
|
+
"Cpu",
|
|
42
|
+
"CpuExtensionError",
|
|
43
|
+
"DasmosError",
|
|
44
|
+
"Disassembler",
|
|
45
|
+
"DisassemblerError",
|
|
46
|
+
"Extension",
|
|
47
|
+
"ExtensionError",
|
|
48
|
+
"FlowControl",
|
|
49
|
+
"IntermediateRepresentation",
|
|
50
|
+
"Opcode",
|
|
51
|
+
"OperandKind",
|
|
52
|
+
"Output",
|
|
53
|
+
"Renderer",
|
|
54
|
+
"RendererExtensionError",
|
|
55
|
+
"StructuredOutput",
|
|
56
|
+
"StructuredRenderer",
|
|
57
|
+
"TextOutput",
|
|
58
|
+
"TextRenderer",
|
|
59
|
+
"cpu_names",
|
|
60
|
+
"create_cpu",
|
|
61
|
+
"create_renderer",
|
|
62
|
+
"describe_cpu",
|
|
63
|
+
"describe_renderer",
|
|
64
|
+
"renderer_names",
|
|
65
|
+
]
|
dasmos/_text.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Internal text utilities.
|
|
2
|
+
|
|
3
|
+
Small helpers used by :mod:`dasmos.extension` for rendering extension
|
|
4
|
+
descriptions. Internal (underscore-prefixed module) — not part of the
|
|
5
|
+
public API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_blank(line: str) -> bool:
|
|
10
|
+
return not line or line.isspace()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def strip_lines(text: str) -> str:
|
|
14
|
+
"""Remove leading and trailing blank lines.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
text: The text to process.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The text with any leading and trailing blank-or-whitespace-only lines
|
|
21
|
+
removed. Interior blank lines are preserved.
|
|
22
|
+
"""
|
|
23
|
+
lines = text.splitlines()
|
|
24
|
+
start = 0
|
|
25
|
+
while start < len(lines) and _is_blank(lines[start]):
|
|
26
|
+
start += 1
|
|
27
|
+
end = len(lines)
|
|
28
|
+
while end > start and _is_blank(lines[end - 1]):
|
|
29
|
+
end -= 1
|
|
30
|
+
return "\n".join(lines[start:end])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_name(name: str) -> str:
|
|
34
|
+
"""Normalise a name by converting hyphens to underscores."""
|
|
35
|
+
return name.replace("-", "_")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def first_line(text: str) -> str:
|
|
39
|
+
"""Extract the first non-empty line from text.
|
|
40
|
+
|
|
41
|
+
Useful for displaying descriptions in tables where multi-line text wraps
|
|
42
|
+
awkwardly.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text: The text to extract the first line from.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The first non-empty line, stripped of leading/trailing whitespace.
|
|
49
|
+
Returns empty string if text is empty or contains only whitespace.
|
|
50
|
+
"""
|
|
51
|
+
if not text:
|
|
52
|
+
return ""
|
|
53
|
+
for line in text.splitlines():
|
|
54
|
+
stripped = line.strip()
|
|
55
|
+
if stripped:
|
|
56
|
+
return stripped
|
|
57
|
+
return ""
|
dasmos/cli.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""Command-line interface for dasmos.
|
|
2
|
+
|
|
3
|
+
The CLI is built on Click and uses :mod:`asyoulikeit` for report
|
|
4
|
+
output, so every introspection command supports
|
|
5
|
+
``--as / --report / --header / --detailed``.
|
|
6
|
+
|
|
7
|
+
Commands fall into three groups:
|
|
8
|
+
|
|
9
|
+
- **Discovery** — ``list-cpus`` / ``describe-cpu`` (and the renderer
|
|
10
|
+
/ environment equivalents). Read-only, useful for finding what's
|
|
11
|
+
installed.
|
|
12
|
+
- **Disassemble** — the headline one-shot
|
|
13
|
+
``dasmos disassemble ROM --load-addr ADDR`` that exercises the
|
|
14
|
+
whole pipeline without requiring a driver script.
|
|
15
|
+
- **Init** — ``dasmos init DRIVER --rom ROM --load-addr ADDR``
|
|
16
|
+
scaffolds a starter driver file the user can edit and run with
|
|
17
|
+
``python DRIVER`` to get the same output as ``disassemble``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
from asyoulikeit import (
|
|
27
|
+
Report,
|
|
28
|
+
Reports,
|
|
29
|
+
ScalarContent,
|
|
30
|
+
TableContent,
|
|
31
|
+
report_output,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from dasmos import __version__
|
|
35
|
+
from dasmos.cpu import CPU_NAMESPACE, cpu_names, describe_cpu
|
|
36
|
+
from dasmos.environment import (
|
|
37
|
+
ENVIRONMENT_NAMESPACE,
|
|
38
|
+
describe_environment,
|
|
39
|
+
environment_names,
|
|
40
|
+
)
|
|
41
|
+
from dasmos.renderer import (
|
|
42
|
+
RENDERER_NAMESPACE,
|
|
43
|
+
describe_renderer,
|
|
44
|
+
renderer_names,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Address parsing — accepts ``0x8000``, ``&8000``, ``$8000``, decimal.
|
|
50
|
+
# Hex prefixes match the conventions a 6502/Acorn user already knows
|
|
51
|
+
# (``&`` = beebasm; ``$`` = ca65/Apple; ``0x`` = C/Python).
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_address(s: str) -> int:
|
|
56
|
+
"""Parse a CLI address argument.
|
|
57
|
+
|
|
58
|
+
Accepts ``0x8000`` / ``&8000`` / ``$8000`` (hex) and bare decimal.
|
|
59
|
+
Raises :class:`ValueError` on anything else so the click error
|
|
60
|
+
path produces a clean usage message.
|
|
61
|
+
"""
|
|
62
|
+
text = s.strip()
|
|
63
|
+
if text.startswith(("0x", "0X")):
|
|
64
|
+
try:
|
|
65
|
+
return int(text, 16)
|
|
66
|
+
except ValueError:
|
|
67
|
+
pass
|
|
68
|
+
elif text.startswith(("&", "$")):
|
|
69
|
+
try:
|
|
70
|
+
return int(text[1:], 16)
|
|
71
|
+
except ValueError:
|
|
72
|
+
pass
|
|
73
|
+
else:
|
|
74
|
+
try:
|
|
75
|
+
return int(text)
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"{s!r} is not a valid address — use 0x1234, &1234, $1234, or decimal"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _AddressParam(click.ParamType):
|
|
84
|
+
"""Click parameter type for an address argument."""
|
|
85
|
+
|
|
86
|
+
name = "ADDR"
|
|
87
|
+
|
|
88
|
+
def convert(self, value, param, ctx):
|
|
89
|
+
try:
|
|
90
|
+
return _parse_address(value)
|
|
91
|
+
except ValueError as exc:
|
|
92
|
+
self.fail(str(exc), param, ctx)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
ADDRESS = _AddressParam()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@click.group()
|
|
99
|
+
@click.version_option(__version__, prog_name="dasmos")
|
|
100
|
+
def cli() -> None:
|
|
101
|
+
"""A pluggable tracing disassembler."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cli.command(name="list-cpus")
|
|
105
|
+
@report_output(reports={
|
|
106
|
+
"cpus": "Registered CPU (processor) plug-ins with one-line descriptions.",
|
|
107
|
+
})
|
|
108
|
+
def list_cpus_command() -> Reports:
|
|
109
|
+
"""List the available CPU plug-ins."""
|
|
110
|
+
table = (
|
|
111
|
+
TableContent(title=f"CPUs registered under {CPU_NAMESPACE!r}")
|
|
112
|
+
.add_column("name", "Name")
|
|
113
|
+
.add_column("description", "Description")
|
|
114
|
+
)
|
|
115
|
+
for name in sorted(cpu_names()):
|
|
116
|
+
table.add_row(
|
|
117
|
+
name=name,
|
|
118
|
+
description=describe_cpu(name, single_line=True),
|
|
119
|
+
)
|
|
120
|
+
return Reports(cpus=Report(data=table))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@cli.command(name="describe-cpu")
|
|
124
|
+
@click.argument("name")
|
|
125
|
+
@report_output(reports={
|
|
126
|
+
"cpu": "The full description of one registered CPU plug-in.",
|
|
127
|
+
})
|
|
128
|
+
def describe_cpu_command(name: str) -> Reports:
|
|
129
|
+
"""Describe a specific CPU plug-in."""
|
|
130
|
+
return Reports(cpu=Report(data=ScalarContent(
|
|
131
|
+
value=describe_cpu(name),
|
|
132
|
+
title=name,
|
|
133
|
+
)))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@cli.command(name="list-renderers")
|
|
137
|
+
@report_output(reports={
|
|
138
|
+
"renderers": "Registered renderer plug-ins with one-line descriptions.",
|
|
139
|
+
})
|
|
140
|
+
def list_renderers_command() -> Reports:
|
|
141
|
+
"""List the available renderer plug-ins."""
|
|
142
|
+
table = (
|
|
143
|
+
TableContent(title=f"Renderers registered under {RENDERER_NAMESPACE!r}")
|
|
144
|
+
.add_column("name", "Name")
|
|
145
|
+
.add_column("description", "Description")
|
|
146
|
+
)
|
|
147
|
+
for name in sorted(renderer_names()):
|
|
148
|
+
table.add_row(
|
|
149
|
+
name=name,
|
|
150
|
+
description=describe_renderer(name, single_line=True),
|
|
151
|
+
)
|
|
152
|
+
return Reports(renderers=Report(data=table))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@cli.command(name="describe-renderer")
|
|
156
|
+
@click.argument("name")
|
|
157
|
+
@report_output(reports={
|
|
158
|
+
"renderer": "The full description of one registered renderer plug-in.",
|
|
159
|
+
})
|
|
160
|
+
def describe_renderer_command(name: str) -> Reports:
|
|
161
|
+
"""Describe a specific renderer plug-in."""
|
|
162
|
+
return Reports(renderer=Report(data=ScalarContent(
|
|
163
|
+
value=describe_renderer(name),
|
|
164
|
+
title=name,
|
|
165
|
+
)))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@cli.command(name="list-environments")
|
|
169
|
+
@report_output(reports={
|
|
170
|
+
"environments": "Registered environment plug-ins with one-line descriptions.",
|
|
171
|
+
})
|
|
172
|
+
def list_environments_command() -> Reports:
|
|
173
|
+
"""List the available environment plug-ins."""
|
|
174
|
+
table = (
|
|
175
|
+
TableContent(title=f"Environments registered under {ENVIRONMENT_NAMESPACE!r}")
|
|
176
|
+
.add_column("name", "Name")
|
|
177
|
+
.add_column("description", "Description")
|
|
178
|
+
)
|
|
179
|
+
for name in sorted(environment_names()):
|
|
180
|
+
table.add_row(
|
|
181
|
+
name=name,
|
|
182
|
+
description=describe_environment(name, single_line=True),
|
|
183
|
+
)
|
|
184
|
+
return Reports(environments=Report(data=table))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@cli.command(name="describe-environment")
|
|
188
|
+
@click.argument("name")
|
|
189
|
+
@report_output(reports={
|
|
190
|
+
"environment": "The full description of one registered environment plug-in.",
|
|
191
|
+
})
|
|
192
|
+
def describe_environment_command(name: str) -> Reports:
|
|
193
|
+
"""Describe a specific environment plug-in."""
|
|
194
|
+
return Reports(environment=Report(data=ScalarContent(
|
|
195
|
+
value=describe_environment(name),
|
|
196
|
+
title=name,
|
|
197
|
+
)))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# disassemble — the headline one-shot command
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _split_envs(env_options: tuple[str, ...]) -> list[str]:
|
|
206
|
+
"""Flatten the ``--env`` option list, accepting both repeated
|
|
207
|
+
flags AND comma-separated values per flag (so ``--env a,b`` and
|
|
208
|
+
``--env a --env b`` are equivalent).
|
|
209
|
+
"""
|
|
210
|
+
out: list[str] = []
|
|
211
|
+
for value in env_options:
|
|
212
|
+
for part in value.split(","):
|
|
213
|
+
part = part.strip()
|
|
214
|
+
if part:
|
|
215
|
+
out.append(part)
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@cli.command(name="disassemble")
|
|
220
|
+
@click.argument("rom", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
221
|
+
@click.option(
|
|
222
|
+
"-a", "--load-addr", "load_addr", type=ADDRESS, required=True,
|
|
223
|
+
help="Address where the binary is loaded (hex with 0x/&/$ prefix or decimal).",
|
|
224
|
+
)
|
|
225
|
+
@click.option(
|
|
226
|
+
"-c", "--cpu", default="nmos6502", show_default=True,
|
|
227
|
+
help="CPU plug-in name (see ``dasmos list-cpus``).",
|
|
228
|
+
)
|
|
229
|
+
@click.option(
|
|
230
|
+
"-r", "--renderer", default="beebasm", show_default=True,
|
|
231
|
+
help="Renderer plug-in name (see ``dasmos list-renderers``).",
|
|
232
|
+
)
|
|
233
|
+
@click.option(
|
|
234
|
+
"-e", "--env", "envs", multiple=True,
|
|
235
|
+
help="Environment plug-in to activate. Repeat or comma-separate "
|
|
236
|
+
"to activate several (see ``dasmos list-environments``).",
|
|
237
|
+
)
|
|
238
|
+
@click.option(
|
|
239
|
+
"--entry", "entries", type=ADDRESS, multiple=True,
|
|
240
|
+
help="Entry-point address to seed the trace from. Repeat for "
|
|
241
|
+
"several. Defaults to the load address.",
|
|
242
|
+
)
|
|
243
|
+
@click.option(
|
|
244
|
+
"-o", "--out", "out_path",
|
|
245
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
246
|
+
help="Write the rendered output to this file. Defaults to stdout.",
|
|
247
|
+
)
|
|
248
|
+
@click.option(
|
|
249
|
+
"--md5", "md5_hash",
|
|
250
|
+
help="Pin the ROM's MD5 hash. The disassembly fails (without "
|
|
251
|
+
"writing output) if the actual hash differs.",
|
|
252
|
+
)
|
|
253
|
+
@click.option(
|
|
254
|
+
"--encoding", "encoding", default="utf-8", show_default=True,
|
|
255
|
+
help="Encoding for the rendered output written via --out. "
|
|
256
|
+
"Default UTF-8. Reading the ROM is unaffected — ROMs are "
|
|
257
|
+
"binary.",
|
|
258
|
+
)
|
|
259
|
+
def disassemble_command(
|
|
260
|
+
rom: Path,
|
|
261
|
+
load_addr: int,
|
|
262
|
+
cpu: str,
|
|
263
|
+
renderer: str,
|
|
264
|
+
envs: tuple[str, ...],
|
|
265
|
+
entries: tuple[int, ...],
|
|
266
|
+
out_path: Path | None,
|
|
267
|
+
md5_hash: str | None,
|
|
268
|
+
encoding: str,
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Disassemble ROM and write the rendered output.
|
|
271
|
+
|
|
272
|
+
Single-shot: builds a Disassembler with the chosen CPU and
|
|
273
|
+
environments, loads ROM at LOAD-ADDR, seeds the trace from each
|
|
274
|
+
--entry (or LOAD-ADDR by default), runs the trace + classification
|
|
275
|
+
+ reference-analysis pipeline, and renders via the chosen
|
|
276
|
+
renderer plug-in.
|
|
277
|
+
|
|
278
|
+
Use ``dasmos init`` to scaffold a driver script you can edit
|
|
279
|
+
instead of re-running this command with growing flag lists.
|
|
280
|
+
"""
|
|
281
|
+
from dasmos.disassembler import Disassembler
|
|
282
|
+
from dasmos.extension import ExtensionError
|
|
283
|
+
|
|
284
|
+
env_list = _split_envs(envs)
|
|
285
|
+
try:
|
|
286
|
+
d = Disassembler.create(cpu=cpu, environments=env_list)
|
|
287
|
+
except ExtensionError as exc:
|
|
288
|
+
raise click.ClickException(str(exc)) from exc
|
|
289
|
+
d.load(rom, load_addr, md5sum=md5_hash)
|
|
290
|
+
seed_addrs = entries if entries else (load_addr,)
|
|
291
|
+
for addr in seed_addrs:
|
|
292
|
+
d.entry(addr)
|
|
293
|
+
output = d.disassemble().render(renderer)
|
|
294
|
+
text = str(output)
|
|
295
|
+
if out_path is not None:
|
|
296
|
+
out_path.write_text(text, encoding=encoding)
|
|
297
|
+
else:
|
|
298
|
+
click.echo(text, nl=False)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# init — scaffold a starter driver script
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
_DRIVER_TEMPLATE = '''\
|
|
307
|
+
"""Disassembly driver for {rom_name}.
|
|
308
|
+
|
|
309
|
+
Generated by ``dasmos init``. Edit this file to add labels,
|
|
310
|
+
comments, and classifications as you analyse the ROM. Run with::
|
|
311
|
+
|
|
312
|
+
python {driver_name}
|
|
313
|
+
|
|
314
|
+
to produce the rendered listing on stdout. See the dasmos docs for
|
|
315
|
+
the full driver-script API and the acornaeology authoring guide
|
|
316
|
+
(``acornaeology.github.io/AUTHORING.md``) for the comment-markdown
|
|
317
|
+
+ memory-map metadata conventions.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
from pathlib import Path
|
|
321
|
+
|
|
322
|
+
import dasmos
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------
|
|
325
|
+
# Configuration
|
|
326
|
+
# ---------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
ROM_PATH = Path({rom_path_repr})
|
|
329
|
+
LOAD_ADDR = {load_addr_hex}{md5_line}
|
|
330
|
+
|
|
331
|
+
# ---------------------------------------------------------------------
|
|
332
|
+
# Build the disassembler
|
|
333
|
+
# ---------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
d = dasmos.Disassembler.create(
|
|
336
|
+
cpu={cpu_repr},
|
|
337
|
+
environments={envs_repr},
|
|
338
|
+
)
|
|
339
|
+
d.load(ROM_PATH, LOAD_ADDR{md5_kwarg})
|
|
340
|
+
|
|
341
|
+
# Entry points — addresses where the trace seeds. Add more as you
|
|
342
|
+
# discover them.
|
|
343
|
+
{entries_block}
|
|
344
|
+
|
|
345
|
+
# ---------------------------------------------------------------------
|
|
346
|
+
# Add your analysis below
|
|
347
|
+
# ---------------------------------------------------------------------
|
|
348
|
+
#
|
|
349
|
+
# As you discover labels, subroutines, and data regions, add them
|
|
350
|
+
# here. The driver-script API:
|
|
351
|
+
#
|
|
352
|
+
# d.label(addr, "name") — required label at addr
|
|
353
|
+
# d.optional_label(addr, "name") — optional (emits only if
|
|
354
|
+
# referenced)
|
|
355
|
+
# d.subroutine(addr, "name", — entry point + label + banner
|
|
356
|
+
# title="...",
|
|
357
|
+
# description="...") — descriptions are full Markdown
|
|
358
|
+
# d.comment(addr, "text") — comment (also Markdown);
|
|
359
|
+
# pass align=Align.INLINE for
|
|
360
|
+
# a trailing remark
|
|
361
|
+
# d.byte(addr, length=N) — classify N bytes as raw data
|
|
362
|
+
# d.string(addr, length=N) — classify N bytes as a string
|
|
363
|
+
# d.constant(value, "name") — name a value (e.g. magic
|
|
364
|
+
# numbers, OSBYTE call IDs)
|
|
365
|
+
#
|
|
366
|
+
# Cross-reference links in comments / descriptions:
|
|
367
|
+
#
|
|
368
|
+
# "see [foo](address:E000)" — bare label link
|
|
369
|
+
# "see [foo](address:E000?hex)" — label + hex literal
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---------------------------------------------------------------------
|
|
373
|
+
# Render
|
|
374
|
+
# ---------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
ir = d.disassemble()
|
|
377
|
+
print(str(ir.render({renderer_repr})))
|
|
378
|
+
'''
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _format_entries_block(entries: tuple[int, ...]) -> str:
|
|
382
|
+
"""Render the entry-point block of the generated driver."""
|
|
383
|
+
if not entries:
|
|
384
|
+
return "d.entry(LOAD_ADDR)"
|
|
385
|
+
lines = [f"d.entry({entry:#06x})" for entry in entries]
|
|
386
|
+
return "\n".join(lines)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@cli.command(name="init")
|
|
390
|
+
@click.argument(
|
|
391
|
+
"driver_path", type=click.Path(dir_okay=False, path_type=Path),
|
|
392
|
+
)
|
|
393
|
+
@click.option(
|
|
394
|
+
"--rom", "rom_path",
|
|
395
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
396
|
+
required=True,
|
|
397
|
+
help="Path to the ROM the driver will disassemble.",
|
|
398
|
+
)
|
|
399
|
+
@click.option(
|
|
400
|
+
"-a", "--load-addr", "load_addr", type=ADDRESS, required=True,
|
|
401
|
+
help="Address where the binary is loaded.",
|
|
402
|
+
)
|
|
403
|
+
@click.option(
|
|
404
|
+
"-c", "--cpu", default="nmos6502", show_default=True,
|
|
405
|
+
help="CPU plug-in name.",
|
|
406
|
+
)
|
|
407
|
+
@click.option(
|
|
408
|
+
"-r", "--renderer", default="beebasm", show_default=True,
|
|
409
|
+
help="Renderer plug-in name to use in the generated driver.",
|
|
410
|
+
)
|
|
411
|
+
@click.option(
|
|
412
|
+
"-e", "--env", "envs", multiple=True,
|
|
413
|
+
help="Environment plug-in to activate (repeatable / comma-separated).",
|
|
414
|
+
)
|
|
415
|
+
@click.option(
|
|
416
|
+
"--entry", "entries", type=ADDRESS, multiple=True,
|
|
417
|
+
help="Entry-point address (repeatable). Defaults to LOAD_ADDR.",
|
|
418
|
+
)
|
|
419
|
+
@click.option(
|
|
420
|
+
"--md5", "md5_hash",
|
|
421
|
+
help="Pin the ROM's MD5 hash in the generated driver.",
|
|
422
|
+
)
|
|
423
|
+
@click.option(
|
|
424
|
+
"--force", is_flag=True,
|
|
425
|
+
help="Overwrite an existing driver file.",
|
|
426
|
+
)
|
|
427
|
+
@click.option(
|
|
428
|
+
"--encoding", "encoding", default="utf-8", show_default=True,
|
|
429
|
+
help="Encoding used to write the generated driver file. "
|
|
430
|
+
"Default UTF-8.",
|
|
431
|
+
)
|
|
432
|
+
def init_command(
|
|
433
|
+
driver_path: Path,
|
|
434
|
+
rom_path: Path,
|
|
435
|
+
load_addr: int,
|
|
436
|
+
cpu: str,
|
|
437
|
+
renderer: str,
|
|
438
|
+
envs: tuple[str, ...],
|
|
439
|
+
entries: tuple[int, ...],
|
|
440
|
+
md5_hash: str | None,
|
|
441
|
+
force: bool,
|
|
442
|
+
encoding: str,
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Scaffold a starter dasmos driver at DRIVER_PATH.
|
|
445
|
+
|
|
446
|
+
The generated driver is FUNCTIONAL — running it with
|
|
447
|
+
``python DRIVER_PATH`` produces the same output as
|
|
448
|
+
``dasmos disassemble`` with the same flags. Edit the file to
|
|
449
|
+
add labels, comments, classifications, and other analysis.
|
|
450
|
+
"""
|
|
451
|
+
if driver_path.exists() and not force:
|
|
452
|
+
raise click.ClickException(
|
|
453
|
+
f"{driver_path} already exists; pass --force to overwrite."
|
|
454
|
+
)
|
|
455
|
+
env_list = _split_envs(envs)
|
|
456
|
+
md5_line = (
|
|
457
|
+
f"\nROM_MD5 = {md5_hash!r}" if md5_hash else ""
|
|
458
|
+
)
|
|
459
|
+
md5_kwarg = ", md5sum=ROM_MD5" if md5_hash else ""
|
|
460
|
+
text = _DRIVER_TEMPLATE.format(
|
|
461
|
+
rom_name=rom_path.name,
|
|
462
|
+
driver_name=driver_path.name,
|
|
463
|
+
# as_posix() so the embedded path is portable: forward slashes
|
|
464
|
+
# work as a Path constructor argument on Windows and don't need
|
|
465
|
+
# backslash-escaping in the str literal.
|
|
466
|
+
rom_path_repr=repr(rom_path.as_posix()),
|
|
467
|
+
load_addr_hex=f"{load_addr:#06x}",
|
|
468
|
+
md5_line=md5_line,
|
|
469
|
+
cpu_repr=repr(cpu),
|
|
470
|
+
envs_repr=repr(env_list),
|
|
471
|
+
md5_kwarg=md5_kwarg,
|
|
472
|
+
entries_block=_format_entries_block(entries),
|
|
473
|
+
renderer_repr=repr(renderer),
|
|
474
|
+
)
|
|
475
|
+
# Always write with an explicit encoding (default UTF-8). On
|
|
476
|
+
# Windows the locale default is cp1252, which would mangle the
|
|
477
|
+
# em-dashes in the template into \x97 bytes — Python would then
|
|
478
|
+
# refuse to run the generated driver since source files must be
|
|
479
|
+
# UTF-8.
|
|
480
|
+
driver_path.write_text(text, encoding=encoding)
|
|
481
|
+
click.echo(f"Wrote driver to {driver_path}", err=True)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
if __name__ == "__main__":
|
|
485
|
+
cli()
|
dasmos/core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Internal core machinery for the dasmos disassembler.
|
|
2
|
+
|
|
3
|
+
Lifted from the py8dis fork with design-smell fixes applied per port:
|
|
4
|
+
module-level globals replaced with instance state, ``sys.exit``-style
|
|
5
|
+
failures replaced with raised exceptions, cyclic imports replaced with
|
|
6
|
+
injected dependencies. Consumers should import from the top-level
|
|
7
|
+
:mod:`dasmos` package — this sub-package is internal and may change.
|
|
8
|
+
"""
|