222 lines
8.1 KiB
Python
222 lines
8.1 KiB
Python
# MIT License
|
|
#
|
|
# Copyright (c) 2023 Neil Webber
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
|
|
import collections
|
|
|
|
#
|
|
# BREAKPOINT FRAMEWORK
|
|
#
|
|
# Arguably this is more complex than necessary; it could have been
|
|
# (and, indeed, was originally) built into the pdp.run() method as a simple
|
|
# set of arguments for stepping N steps or stopping at a given PC and
|
|
# nothing fancier than that could be specified.
|
|
#
|
|
# However, this generalization now allows arbitrary breakpoint processing.
|
|
# The run() method will call a breakpoint after each instruction and
|
|
# pass in the machine object plus information, 'XInfo', containing
|
|
# important internal execution state of the machine:
|
|
|
|
XInfo = collections.namedtuple('XInfo', ['PC', 'instruction'])
|
|
|
|
# The Breakpoint object can use that information to cause a breakpoint.
|
|
# Custom Breakpoint objects can contain state, and anything that can
|
|
# be implemented as a per-instruction-boundary test is now possible.
|
|
# The breakpoint objects can also store logs of relevant state information
|
|
# (see, for example, Lookback).
|
|
#
|
|
# CAUTION regarding breakpoint state data and object re-use. If a
|
|
# breakpoint object is created once but supplied to multiple pdp.run()
|
|
# method calls, it (obviously?) will carry its previous state forward.
|
|
# It is not initialized anew each call to pdp.run(). Thus, for example,
|
|
# if it is a "run N steps" breakpoint and has partially (or completely)
|
|
# counted down, the count does not start over with the second call.
|
|
# This may or may not be what was desired. Code accordingly. Note that
|
|
# run_steps() and run_until() create a new breakpoint object every time
|
|
# and avoid this issue that way.
|
|
#
|
|
|
|
|
|
class Breakpoint:
|
|
"""Base Breakpoint - a null breakpoint that never fires."""
|
|
def __call__(self, pdp, xinfo):
|
|
return False
|
|
|
|
|
|
class StepsBreakpoint(Breakpoint):
|
|
"""Break after the given number of instructions."""
|
|
|
|
def __init__(self, steps):
|
|
"""steps is the number of instructions until breakpoint."""
|
|
if steps < 1:
|
|
raise ValueError(f"steps ({steps}) must be at least 1")
|
|
self.togo = steps
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
self.togo -= 1
|
|
return self.togo == 0
|
|
|
|
|
|
class PCBreakpoint(Breakpoint):
|
|
"""Break at the given PC"""
|
|
|
|
def __init__(self, *, stoppc, stopmode=None):
|
|
"""stoppc is the pc location to break at.
|
|
optional stopmode restricts the breakpoint to the given
|
|
mode (0, 1, 3 for Kernel/Supervisor/User)
|
|
"""
|
|
self.stoppc = stoppc
|
|
self.stopmf = lambda m: (stopmode is None) or (m == stopmode)
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
# NOTE: xinfo.PC is the PC of the instruction just completed,
|
|
# whereas pdp.r[pdp.PC] is the PC of the next instruction
|
|
# to be executed.
|
|
return pdp.r[pdp.PC] == self.stoppc and self.stopmf(pdp.psw_curmode)
|
|
|
|
|
|
# Fire on the Nth occurrence of the given breakpoint
|
|
class NthBreakpoint(Breakpoint):
|
|
"""Break after the nth occurrence of a given breakpoint"""
|
|
|
|
def __init__(self, bp, nth, /):
|
|
"""Break when 'bp' has occurred 'nth' times."""
|
|
if nth < 1:
|
|
raise ValueError(f"nth ({nth}) must be at least 1")
|
|
self.__nth = self.__count = nth
|
|
self.__bp = bp
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
if self.__bp(pdp, xinfo):
|
|
self.__count -= 1
|
|
return self.__count == 0
|
|
|
|
|
|
# Add lookback state to a given breakpoint.
|
|
# ALTERNATIVELY, can be used entirely by itself (bp=None), and will provide
|
|
# lookback if the run() loop terminates for any reason (e.g., a HALT).
|
|
#
|
|
class Lookback(Breakpoint):
|
|
"""Records machine state after each instruction."""
|
|
|
|
def __init__(self, bp=None, /, lookbacks=100):
|
|
"""Augments breakpoint 'bp' with a lookback record of machine state.
|
|
|
|
'lookbacks' (default == 100) is how far back state is kept. The
|
|
record is FIFO (i.e., the last N states are kept)
|
|
|
|
See states property for obtaining the lookback record.
|
|
"""
|
|
self.__backstates = collections.deque([], lookbacks)
|
|
self.__bp = bp or (lambda pdp, xinfo: False)
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
self.__backstates.append((xinfo, pdp.machinestate()))
|
|
return self.__bp(pdp, xinfo)
|
|
|
|
@property
|
|
def states(self):
|
|
"""Return the lookback states."""
|
|
return list(self.__backstates)
|
|
|
|
|
|
class MultiBreakpoint(Breakpoint):
|
|
"""A Breakpoint that fires if any of the contained breakpoints fire."""
|
|
|
|
def __init__(self, bp0, /, *bps, testall=True):
|
|
"""MultiBreakpoint(bp0, *bps, testall=True)
|
|
|
|
If testall is true (the default) all breakpoints are tested each
|
|
time no matter what. This is generally desired because breakpoint
|
|
tests often have side effects (e.g., a "steps until fires" counter
|
|
is decremented, or log entries are made).
|
|
|
|
If not testall, breakpoints after one that fires are not tested.
|
|
"""
|
|
|
|
self.testall = testall
|
|
self.bkpts = [bp0] + list(bps)
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
stophere = False
|
|
for bp in self.bkpts:
|
|
if bp(pdp, xinfo):
|
|
stophere = True
|
|
if not self.testall:
|
|
break
|
|
return stophere
|
|
|
|
|
|
# Add instruction logging to a given breakpoint.
|
|
# To use: create the underlying breakpoint and then pass it into this:
|
|
# bp = Logger(SomeOtherBreakpoint(other-args))
|
|
#
|
|
# If used with no underlying breakpoint, just logs instructions.
|
|
# bp = Logger()
|
|
#
|
|
class Logger(Breakpoint):
|
|
"""Log every instruction. Caution: LARGE logs, slow performance."""
|
|
|
|
def __init__(self, bp=None, /, *, logger=None):
|
|
self.__bp = bp or (lambda pdp, info: False)
|
|
self.__logger = logger
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
if self.__logger is None:
|
|
self.__logger = pdp.logger
|
|
|
|
m_s = "KS!U"[pdp.psw_curmode]
|
|
pc_s = oct(xinfo.PC)
|
|
inst = xinfo.instruction
|
|
inst_s = "None" if inst is None else oct(inst)
|
|
self.__logger.debug(f"{pc_s}/{m_s} :: {inst_s}")
|
|
return self.__bp(pdp, xinfo)
|
|
|
|
|
|
# Useful for ensuring the emulation implementation isn't broken in
|
|
# some way - making sure all values in physical memory are 16 bits (only).
|
|
# This will run an expensive check every nth instructions.
|
|
# Obviously, if bogus values are found it will be nearly impossible to
|
|
# figure out HOW they got in there; put some checks back into mmu word/byteRW
|
|
# functions as necessary to debug that (they are removed for performance).
|
|
|
|
class _MemChecker(Breakpoint):
|
|
def __init__(self, nth):
|
|
self.check_every = nth
|
|
self.togo = nth
|
|
|
|
def __call__(self, pdp, xinfo):
|
|
self.togo -= 1
|
|
if self.togo == 0:
|
|
self.check16(pdp)
|
|
self.togo = self.check_every
|
|
return False
|
|
|
|
def check16(self, pdp):
|
|
"""Verifies no illegal values ended up in physical memory or regs."""
|
|
pdp.logger.debug("check16 checking physical memory")
|
|
for a, w in enumerate(pdp.physmem):
|
|
if w < 0 or w > 65535:
|
|
raise ValueError(f"Illegal physmem value {w} @ {oct(a<<1)}")
|
|
for r, w in enumerate(pdp.r):
|
|
if w < 0 or w > 65535:
|
|
raise ValueError(f"Illegal reg value {w} @ {r}")
|