mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
261 lines
6.7 KiB
Go
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.LastCPUAddressLiteral && wtc.lastAddressWrite == wtc.dbg.vcs.Mem.LastCPUWrite {
|
|
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.LastCPUAddressLiteral
|
|
watchAddress = w.ai.Address
|
|
} else {
|
|
accessAddress = wtc.dbg.vcs.Mem.LastCPUAddressMapped
|
|
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.LastCPUData {
|
|
continue
|
|
}
|
|
|
|
if w.ai.Read {
|
|
if !wtc.dbg.vcs.Mem.LastCPUWrite {
|
|
checkString.WriteString(fmt.Sprintf("watch at %s (read value %#02x)\n", w, wtc.dbg.vcs.Mem.LastCPUData))
|
|
}
|
|
} else {
|
|
if wtc.dbg.vcs.Mem.LastCPUWrite {
|
|
checkString.WriteString(fmt.Sprintf("watch at %s (written value %#02x)\n", w, wtc.dbg.vcs.Mem.LastCPUData))
|
|
}
|
|
}
|
|
}
|
|
|
|
// note what the last address accessed was
|
|
wtc.lastAddressAccessed = wtc.dbg.vcs.Mem.LastCPUAddressLiteral
|
|
wtc.lastAddressWrite = wtc.dbg.vcs.Mem.LastCPUWrite
|
|
|
|
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
|
|
}
|