Gopher2600/debugger/halt_watches.go
JetSetIlly c19a37a738 CLK breakpoints/targets will prevent debugger entering playmode
two reasons:

1) to keep performance acceptable playmode only checks halting
   conditions on a CPU instruction boundary. however a CLK changes many
   times during an instruction meaning it will never match.

2) a CLK breakpoint will always match within 228 emulation ticks so
   there is no point entering playmode at all because it will definitely
   drop back to the debugger (within microseconds)

added a range change check to SCANLINE and CLK targets in
breakpoints.parseCommand(). we know what the possible values are for
these targets so we can be helpful and inform the user the some values
will never match
2021-11-11 10:00:26 +00:00

261 lines
6.7 KiB
Go

// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package debugger
import (
"fmt"
"strconv"
"strings"
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/debugger/dbgmem"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
)
type watcher struct {
ai dbgmem.AddressInfo
// whether to watch for a specific value. a matchValue of false means the
// watcher will match regardless of the value
matchValue bool
value uint8
// whether the address should be interpreted strictly or whether mirrors
// should be considered too
strict bool
}
func (w watcher) String() string {
val := ""
if w.matchValue {
val = fmt.Sprintf(" (value=%#02x)", w.value)
}
event := "write"
if w.ai.Read {
event = "read"
}
strict := ""
if w.strict {
strict = " (strict)"
}
return fmt.Sprintf("%s %s%s%s", w.ai, event, val, strict)
}
// the list of currently defined watches in the system.
type watches struct {
dbg *Debugger
watches []watcher
lastAddressAccessed uint16
lastAddressWrite bool
}
// newWatches is the preferred method of initialisation for the watches type.
func newWatches(dbg *Debugger) *watches {
wtc := &watches{
dbg: dbg,
}
wtc.clear()
return wtc
}
// clear all watches.
func (wtc *watches) clear() {
wtc.watches = make([]watcher, 0, 10)
}
// drop a specific watcher by a position in the list.
func (wtc *watches) drop(num int) error {
if len(wtc.watches)-1 < num {
return curated.Errorf("watch #%d is not defined", num)
}
h := wtc.watches[:num]
t := wtc.watches[num+1:]
wtc.watches = make([]watcher, len(h)+len(t), cap(wtc.watches))
copy(wtc.watches, h)
copy(wtc.watches[len(h):], t)
return nil
}
// check compares the current state of the emulation with every watch
// condition. returns a string listing every condition that matches (separated
// by \n).
func (wtc *watches) check() string {
if len(wtc.watches) == 0 {
return ""
}
// no check for phantom access
if wtc.dbg.vcs.CPU.PhantomMemAccess {
return ""
}
// no check if access address & write flag haven't changed
//
// note that the write flag comparison is required otherwise RMW
// instructions will not be caught on the write signal (which would mean
// that a WRITE watch will never match a RMW instruction)
if wtc.lastAddressAccessed == wtc.dbg.vcs.Mem.LastAccessAddress && wtc.lastAddressWrite == wtc.dbg.vcs.Mem.LastAccessWrite {
return ""
}
checkString := strings.Builder{}
for _, w := range wtc.watches {
var accessAddress uint16
var watchAddress uint16
// pick which addresses to comare depending on whether watch is strict
if w.strict {
accessAddress = wtc.dbg.vcs.Mem.LastAccessAddress
watchAddress = w.ai.Address
} else {
accessAddress = wtc.dbg.vcs.Mem.LastAccessAddressMapped
watchAddress = w.ai.MappedAddress
}
// continue loop if we're not matching last address accessed
if watchAddress != accessAddress {
continue
}
if w.matchValue && w.value != wtc.dbg.vcs.Mem.LastAccessValue {
continue
}
if w.ai.Read {
if !wtc.dbg.vcs.Mem.LastAccessWrite {
checkString.WriteString(fmt.Sprintf("watch at %s (read value %#02x)\n", w, wtc.dbg.vcs.Mem.LastAccessValue))
}
} else {
if wtc.dbg.vcs.Mem.LastAccessWrite {
checkString.WriteString(fmt.Sprintf("watch at %s (written value %#02x)\n", w, wtc.dbg.vcs.Mem.LastAccessValue))
}
}
}
// note what the last address accessed was
wtc.lastAddressAccessed = wtc.dbg.vcs.Mem.LastAccessAddress
wtc.lastAddressWrite = wtc.dbg.vcs.Mem.LastAccessWrite
return checkString.String()
}
// list currently defined watches.
func (wtc *watches) list() {
if len(wtc.watches) == 0 {
wtc.dbg.printLine(terminal.StyleFeedback, "no watches")
} else {
wtc.dbg.printLine(terminal.StyleFeedback, "watches:")
for i := range wtc.watches {
wtc.dbg.printLine(terminal.StyleFeedback, "% 2d: %s", i, wtc.watches[i])
}
}
}
// parse tokens and add new watch. unlike breakpoints and traps, only one watch
// at a time can be specified on the command line.
func (wtc *watches) parseCommand(tokens *commandline.Tokens) error {
var read bool
var strict bool
// event type
arg, _ := tokens.Get()
arg = strings.ToUpper(arg)
switch arg {
case "READ":
read = true
case "WRITE":
read = false
default:
// default watch event is READ
read = true
tokens.Unget()
}
// strict addressing or not
arg, _ = tokens.Get()
arg = strings.ToUpper(arg)
switch arg {
case "STRICT":
strict = true
default:
strict = false
tokens.Unget()
}
// get address. required.
a, _ := tokens.Get()
// convert address
var ai *dbgmem.AddressInfo
if read {
ai = wtc.dbg.dbgmem.MapAddress(a, true)
} else {
ai = wtc.dbg.dbgmem.MapAddress(a, false)
}
// mapping of the address was unsuccessful
if ai == nil {
if read {
return curated.Errorf("invalid watch address (%s) expecting 16-bit address or a read symbol", a)
}
return curated.Errorf("invalid watch address (%s) expecting 16-bit address or a write symbol", a)
}
// get value if possible
var val uint64
var err error
v, useVal := tokens.Get()
if useVal {
val, err = strconv.ParseUint(v, 0, 8)
if err != nil {
return curated.Errorf("invalid watch value (%s) expecting 8-bit value", a)
}
}
nw := watcher{
ai: *ai,
matchValue: useVal,
value: uint8(val),
strict: strict,
}
// check to see if watch already exists
for _, w := range wtc.watches {
// the conditions for a watch matching are very specific: both must
// have the same address, be the same /type/ of address (read or
// write), and the same watch value (if applicable)
//
// note that this method means we can add a watch that is a subset of
// an existing watch (or vice-versa) but that's okay, the check()
// function will list all matches. plus, if we combine two watches such
// that only the larger set remains, it may confuse the user
if w.ai.Address == nw.ai.Address &&
w.ai.Read == nw.ai.Read &&
w.matchValue == nw.matchValue && w.value == nw.value {
return curated.Errorf("already being watched (%s)", w)
}
}
// add watch
wtc.watches = append(wtc.watches, nw)
return nil
}