m68000-python 0.1.0__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.
- m68000_python/__init__.py +73 -0
- m68000_python/__main__.py +112 -0
- m68000_python/_alu.py +656 -0
- m68000_python/_bcd.py +137 -0
- m68000_python/_bits.py +100 -0
- m68000_python/_control.py +208 -0
- m68000_python/_core.py +545 -0
- m68000_python/_dispatch.py +243 -0
- m68000_python/_ea.py +177 -0
- m68000_python/_flags.py +152 -0
- m68000_python/_loads.py +405 -0
- m68000_python/_shifts.py +168 -0
- m68000_python/_system.py +314 -0
- m68000_python/conformance.py +796 -0
- m68000_python/console.py +254 -0
- m68000_python/cpu.py +373 -0
- m68000_python/debug.py +361 -0
- m68000_python/disasm.py +354 -0
- m68000_python/py.typed +0 -0
- m68000_python/state.py +76 -0
- m68000_python/trace.py +296 -0
- m68000_python-0.1.0.dist-info/METADATA +379 -0
- m68000_python-0.1.0.dist-info/RECORD +25 -0
- m68000_python-0.1.0.dist-info/WHEEL +4 -0
- m68000_python-0.1.0.dist-info/licenses/LICENSE +21 -0
m68000_python/debug.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Dependency-free execution control and structured debugging records.
|
|
2
|
+
|
|
3
|
+
``DebugSession`` drives an existing CPU -- any object with ``step()`` and
|
|
4
|
+
``capture_state()`` -- one boundary at a time, with execute breakpoints,
|
|
5
|
+
bounded runs, a history ring and, optionally, memory-access tracking and
|
|
6
|
+
watchpoints. It never changes what the core does. See docs/debug-session.md.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from collections.abc import Callable, Iterator
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from m68000_python.disasm import Instruction, WordReader, disassemble
|
|
16
|
+
from m68000_python.state import CPUState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class DebugTarget(Protocol):
|
|
21
|
+
"""The minimum a :class:`DebugSession` needs from a CPU."""
|
|
22
|
+
|
|
23
|
+
def step(self) -> int:
|
|
24
|
+
"""Advance one instruction or exception boundary; return its clocks."""
|
|
25
|
+
|
|
26
|
+
def capture_state(self) -> CPUState:
|
|
27
|
+
"""Capture the current CPU-owned state."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BoundaryKind(Enum):
|
|
31
|
+
"""What one ``step()`` did."""
|
|
32
|
+
|
|
33
|
+
INSTRUCTION = "instruction"
|
|
34
|
+
TRACE = "trace" # the trace exception after a traced instruction
|
|
35
|
+
INTERRUPT = "interrupt" # an interrupt accepted
|
|
36
|
+
STOPPED_IDLE = "stopped_idle" # clocks spent inside STOP
|
|
37
|
+
HALTED_IDLE = "halted_idle" # clocks spent halted by a double bus fault
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StopReason(Enum):
|
|
41
|
+
"""Why a bounded run returned control."""
|
|
42
|
+
|
|
43
|
+
BREAKPOINT = "breakpoint" # before the instruction at a breakpoint
|
|
44
|
+
WATCHPOINT = "watchpoint" # after the step that touched a watched address
|
|
45
|
+
STOPPED = "stopped" # inside STOP with no interrupt able to end it
|
|
46
|
+
HALTED = "halted" # double bus fault; only reset leaves it
|
|
47
|
+
STEP_LIMIT = "step_limit"
|
|
48
|
+
CYCLE_LIMIT = "cycle_limit"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
#: One bus access: ("r" or "w", address, value, size in bytes).
|
|
52
|
+
Access = tuple[str, int, int, int]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class StepRecord:
|
|
57
|
+
"""Immutable before/after evidence for one boundary."""
|
|
58
|
+
|
|
59
|
+
sequence: int
|
|
60
|
+
kind: BoundaryKind
|
|
61
|
+
before: CPUState
|
|
62
|
+
after: CPUState
|
|
63
|
+
cycles: int
|
|
64
|
+
instruction: Instruction | None
|
|
65
|
+
#: Every bus access the step made, in order, when the session tracks
|
|
66
|
+
#: accesses; ``None`` otherwise.
|
|
67
|
+
accesses: tuple[Access, ...] | None = None
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
if type(self.sequence) is not int or self.sequence < 0:
|
|
71
|
+
raise ValueError("sequence must be a non-negative integer")
|
|
72
|
+
if type(self.kind) is not BoundaryKind:
|
|
73
|
+
raise ValueError("kind must be a BoundaryKind")
|
|
74
|
+
if type(self.before) is not CPUState or type(self.after) is not CPUState:
|
|
75
|
+
raise ValueError("before and after must be CPUState values")
|
|
76
|
+
if type(self.cycles) is not int or self.cycles <= 0:
|
|
77
|
+
raise ValueError("cycles must be a positive integer")
|
|
78
|
+
if self.kind is not BoundaryKind.INSTRUCTION and self.instruction is not None:
|
|
79
|
+
raise ValueError("only instruction boundaries carry an instruction")
|
|
80
|
+
if self.instruction is not None and type(self.instruction) is not Instruction:
|
|
81
|
+
raise ValueError("instruction must be an Instruction or None")
|
|
82
|
+
if self.accesses is not None and (
|
|
83
|
+
type(self.accesses) is not tuple or not all(_is_access(a) for a in self.accesses)
|
|
84
|
+
):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
'accesses must be a tuple of ("r" or "w", address, value, size 1 or 2)'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class RunResult:
|
|
92
|
+
"""Summary of one bounded run."""
|
|
93
|
+
|
|
94
|
+
reason: StopReason
|
|
95
|
+
steps: int
|
|
96
|
+
instructions: int
|
|
97
|
+
cycles: int
|
|
98
|
+
state: CPUState
|
|
99
|
+
last_record: StepRecord | None
|
|
100
|
+
#: For WATCHPOINT: the watched accesses that stopped the run.
|
|
101
|
+
hits: tuple[Access, ...] = ()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def next_boundary(state: CPUState) -> BoundaryKind:
|
|
105
|
+
"""What the next ``step()`` will do, decided exactly as ``M68000CPU.step()`` decides it."""
|
|
106
|
+
if state.halted:
|
|
107
|
+
return BoundaryKind.HALTED_IDLE
|
|
108
|
+
if state.trace_pending:
|
|
109
|
+
return BoundaryKind.TRACE
|
|
110
|
+
if state.ipl and (state.ipl > (state.sr >> 8) & 7 or (state.ipl == 7 and state.nmi_edge)):
|
|
111
|
+
return BoundaryKind.INTERRUPT
|
|
112
|
+
if state.stopped:
|
|
113
|
+
return BoundaryKind.STOPPED_IDLE
|
|
114
|
+
return BoundaryKind.INSTRUCTION
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class DebugSession:
|
|
118
|
+
"""Control an existing CPU without modifying its execution core.
|
|
119
|
+
|
|
120
|
+
``peek_word`` must be side-effect-free (the host's memory, not its bus);
|
|
121
|
+
without it stepping and breakpoints work but records carry no
|
|
122
|
+
disassembly. ``track_accesses=True`` re-attaches the CPU's four bus
|
|
123
|
+
callables through wrappers that record every access and enable
|
|
124
|
+
watchpoints; :meth:`close` puts the originals back.
|
|
125
|
+
|
|
126
|
+
The target may be a whole board rather than a bare CPU: an object whose
|
|
127
|
+
``step()`` runs its devices around one CPU step and whose ``cpu``
|
|
128
|
+
attribute is the processor.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
target: DebugTarget,
|
|
134
|
+
*,
|
|
135
|
+
peek_word: WordReader | None = None,
|
|
136
|
+
history_limit: int = 256,
|
|
137
|
+
track_accesses: bool = False,
|
|
138
|
+
) -> None:
|
|
139
|
+
if not isinstance(target, DebugTarget):
|
|
140
|
+
raise TypeError("target must provide step() and capture_state()")
|
|
141
|
+
if peek_word is not None and not callable(peek_word):
|
|
142
|
+
raise TypeError("peek_word must be callable or None")
|
|
143
|
+
if type(history_limit) is not int or history_limit < 0:
|
|
144
|
+
raise ValueError("history_limit must be a non-negative integer")
|
|
145
|
+
self.target = target
|
|
146
|
+
#: The processor: the target itself, or the ``cpu`` a board target carries.
|
|
147
|
+
self.cpu = getattr(target, "cpu", target)
|
|
148
|
+
self.peek_word = peek_word
|
|
149
|
+
self.history_limit = history_limit
|
|
150
|
+
self.breakpoints: set[int] = set()
|
|
151
|
+
self.watchpoints: dict[int, str] = {} # address -> "r", "w" or "rw"
|
|
152
|
+
self.total_steps = 0
|
|
153
|
+
self.total_instructions = 0
|
|
154
|
+
self.total_cycles = 0
|
|
155
|
+
self._history: deque[StepRecord] = deque(maxlen=history_limit or 1)
|
|
156
|
+
self._accesses: list[Access] | None = None
|
|
157
|
+
self._originals: tuple[Callable, ...] | None = None
|
|
158
|
+
if track_accesses:
|
|
159
|
+
self._wrap_bus()
|
|
160
|
+
|
|
161
|
+
# -- access tracking ---------------------------------------------------
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def tracking(self) -> bool:
|
|
165
|
+
return self._originals is not None
|
|
166
|
+
|
|
167
|
+
def _wrap_bus(self) -> None:
|
|
168
|
+
cpu = self.cpu
|
|
169
|
+
read_byte, read_word = cpu.read_byte, cpu.read_word
|
|
170
|
+
write_byte, write_word = cpu.write_byte, cpu.write_word
|
|
171
|
+
self._originals = (read_byte, read_word, write_byte, write_word)
|
|
172
|
+
self._accesses = []
|
|
173
|
+
log = self._accesses
|
|
174
|
+
|
|
175
|
+
def tracked_read_byte(address: int, **kw) -> int:
|
|
176
|
+
value = read_byte(address, **kw)
|
|
177
|
+
log.append(("r", address, value, 1))
|
|
178
|
+
return value
|
|
179
|
+
|
|
180
|
+
def tracked_read_word(address: int, **kw) -> int:
|
|
181
|
+
value = read_word(address, **kw)
|
|
182
|
+
log.append(("r", address, value, 2))
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
def tracked_write_byte(address: int, value: int, **kw) -> None:
|
|
186
|
+
log.append(("w", address, value, 1))
|
|
187
|
+
write_byte(address, value, **kw)
|
|
188
|
+
|
|
189
|
+
def tracked_write_word(address: int, value: int, **kw) -> None:
|
|
190
|
+
log.append(("w", address, value, 2))
|
|
191
|
+
write_word(address, value, **kw)
|
|
192
|
+
|
|
193
|
+
cpu.attach_bus(tracked_read_byte, tracked_read_word, tracked_write_byte, tracked_write_word)
|
|
194
|
+
|
|
195
|
+
def close(self) -> None:
|
|
196
|
+
"""Stop tracking accesses and give the CPU its own bus callables back."""
|
|
197
|
+
if self._originals is not None:
|
|
198
|
+
self.cpu.attach_bus(*self._originals)
|
|
199
|
+
self._originals = None
|
|
200
|
+
self._accesses = None
|
|
201
|
+
|
|
202
|
+
# -- breakpoints and watchpoints ---------------------------------------
|
|
203
|
+
|
|
204
|
+
def add_breakpoint(self, address: int) -> None:
|
|
205
|
+
"""Stop before executing the instruction at ``address``."""
|
|
206
|
+
self.breakpoints.add(_address(address))
|
|
207
|
+
|
|
208
|
+
def remove_breakpoint(self, address: int) -> None:
|
|
209
|
+
self.breakpoints.discard(_address(address))
|
|
210
|
+
|
|
211
|
+
def add_watchpoint(self, address: int, kind: str = "rw") -> None:
|
|
212
|
+
"""Stop after any step that reads (``"r"``), writes (``"w"``) or either (``"rw"``).
|
|
213
|
+
|
|
214
|
+
A word access watches both of its bytes.
|
|
215
|
+
"""
|
|
216
|
+
if not self.tracking:
|
|
217
|
+
raise ValueError("watchpoints need a session created with track_accesses=True")
|
|
218
|
+
if kind not in ("r", "w", "rw"):
|
|
219
|
+
raise ValueError('kind must be "r", "w" or "rw"')
|
|
220
|
+
self.watchpoints[_address(address)] = kind
|
|
221
|
+
|
|
222
|
+
def remove_watchpoint(self, address: int) -> None:
|
|
223
|
+
self.watchpoints.pop(_address(address), None)
|
|
224
|
+
|
|
225
|
+
# -- history -----------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def history(self) -> tuple[StepRecord, ...]:
|
|
229
|
+
"""Bounded immutable view of the retained records, oldest first."""
|
|
230
|
+
return tuple(self._history)
|
|
231
|
+
|
|
232
|
+
def iter_history(self, *, newest_first: bool = False) -> Iterator[StepRecord]:
|
|
233
|
+
return reversed(self._history) if newest_first else iter(self._history)
|
|
234
|
+
|
|
235
|
+
def clear_history(self) -> None:
|
|
236
|
+
self._history.clear()
|
|
237
|
+
|
|
238
|
+
# -- execution ---------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def step(self) -> StepRecord:
|
|
241
|
+
"""Advance exactly one boundary, ignoring breakpoints."""
|
|
242
|
+
before = self.target.capture_state()
|
|
243
|
+
kind = next_boundary(before)
|
|
244
|
+
instruction = None
|
|
245
|
+
if kind is BoundaryKind.INSTRUCTION and self.peek_word is not None:
|
|
246
|
+
instruction = disassemble(self.peek_word, before.pc)
|
|
247
|
+
if self._accesses is not None:
|
|
248
|
+
self._accesses.clear()
|
|
249
|
+
cycles = self.target.step()
|
|
250
|
+
if type(cycles) is not int or cycles <= 0:
|
|
251
|
+
raise ValueError("target step() must return a positive cycle count")
|
|
252
|
+
record = StepRecord(
|
|
253
|
+
sequence=self.total_steps,
|
|
254
|
+
kind=kind,
|
|
255
|
+
before=before,
|
|
256
|
+
after=self.target.capture_state(),
|
|
257
|
+
cycles=cycles,
|
|
258
|
+
instruction=instruction,
|
|
259
|
+
accesses=None if self._accesses is None else tuple(self._accesses),
|
|
260
|
+
)
|
|
261
|
+
self.total_steps += 1
|
|
262
|
+
self.total_cycles += cycles
|
|
263
|
+
if kind is BoundaryKind.INSTRUCTION:
|
|
264
|
+
self.total_instructions += 1
|
|
265
|
+
if self.history_limit:
|
|
266
|
+
self._history.append(record)
|
|
267
|
+
return record
|
|
268
|
+
|
|
269
|
+
def run(
|
|
270
|
+
self,
|
|
271
|
+
*,
|
|
272
|
+
max_steps: int,
|
|
273
|
+
max_cycles: int | None = None,
|
|
274
|
+
stop_on_stop: bool = True,
|
|
275
|
+
stop_on_halt: bool = True,
|
|
276
|
+
) -> RunResult:
|
|
277
|
+
"""Run until a stop condition or the mandatory step budget.
|
|
278
|
+
|
|
279
|
+
Breakpoints stop *before* the instruction; watchpoints stop *after*
|
|
280
|
+
the step that touched the address. A cycle limit is checked after each
|
|
281
|
+
atomic step and may be exceeded by that step's cost.
|
|
282
|
+
"""
|
|
283
|
+
if type(max_steps) is not int or max_steps <= 0:
|
|
284
|
+
raise ValueError("max_steps must be a positive integer")
|
|
285
|
+
if max_cycles is not None and (type(max_cycles) is not int or max_cycles <= 0):
|
|
286
|
+
raise ValueError("max_cycles must be a positive integer or None")
|
|
287
|
+
steps = instructions = cycles = 0
|
|
288
|
+
last = None
|
|
289
|
+
|
|
290
|
+
def result(reason: StopReason, state: CPUState, **extra) -> RunResult:
|
|
291
|
+
return RunResult(reason, steps, instructions, cycles, state, last, **extra)
|
|
292
|
+
|
|
293
|
+
while steps < max_steps:
|
|
294
|
+
state = self.target.capture_state()
|
|
295
|
+
kind = next_boundary(state)
|
|
296
|
+
if (
|
|
297
|
+
kind is BoundaryKind.INSTRUCTION
|
|
298
|
+
and state.pc & 0xFFFFFF in self.breakpoints
|
|
299
|
+
and steps
|
|
300
|
+
):
|
|
301
|
+
return result(StopReason.BREAKPOINT, state)
|
|
302
|
+
if stop_on_stop and kind is BoundaryKind.STOPPED_IDLE:
|
|
303
|
+
return result(StopReason.STOPPED, state)
|
|
304
|
+
if stop_on_halt and kind is BoundaryKind.HALTED_IDLE:
|
|
305
|
+
return result(StopReason.HALTED, state)
|
|
306
|
+
last = self.step()
|
|
307
|
+
steps += 1
|
|
308
|
+
cycles += last.cycles
|
|
309
|
+
if last.kind is BoundaryKind.INSTRUCTION:
|
|
310
|
+
instructions += 1
|
|
311
|
+
hits = self._watch_hits(last)
|
|
312
|
+
if hits:
|
|
313
|
+
return result(StopReason.WATCHPOINT, last.after, hits=hits)
|
|
314
|
+
if max_cycles is not None and cycles >= max_cycles:
|
|
315
|
+
return result(StopReason.CYCLE_LIMIT, last.after)
|
|
316
|
+
return result(StopReason.STEP_LIMIT, last.after if last else self.target.capture_state())
|
|
317
|
+
|
|
318
|
+
def _watch_hits(self, record: StepRecord) -> tuple[Access, ...]:
|
|
319
|
+
if not self.watchpoints or not record.accesses:
|
|
320
|
+
return ()
|
|
321
|
+
watched = self.watchpoints
|
|
322
|
+
return tuple(
|
|
323
|
+
access
|
|
324
|
+
for access in record.accesses
|
|
325
|
+
if any(
|
|
326
|
+
(access[1] + offset) in watched and access[0] in watched[access[1] + offset]
|
|
327
|
+
for offset in range(access[3])
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _is_access(access: object) -> bool:
|
|
333
|
+
if type(access) is not tuple or len(access) != 4:
|
|
334
|
+
return False
|
|
335
|
+
kind, address, value, size = access
|
|
336
|
+
return (
|
|
337
|
+
kind in ("r", "w")
|
|
338
|
+
and type(address) is int
|
|
339
|
+
and 0 <= address <= 0xFFFFFF
|
|
340
|
+
and type(value) is int
|
|
341
|
+
and value >= 0
|
|
342
|
+
and size in (1, 2)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _address(address: int) -> int:
|
|
347
|
+
if type(address) is not int or not 0 <= address <= 0xFFFFFF:
|
|
348
|
+
raise ValueError("address must be an integer in range 0x000000..0xFFFFFF")
|
|
349
|
+
return address
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
__all__ = [
|
|
353
|
+
"Access",
|
|
354
|
+
"BoundaryKind",
|
|
355
|
+
"DebugSession",
|
|
356
|
+
"DebugTarget",
|
|
357
|
+
"RunResult",
|
|
358
|
+
"StepRecord",
|
|
359
|
+
"StopReason",
|
|
360
|
+
"next_boundary",
|
|
361
|
+
]
|