mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 12:08:01 -04:00
83ace43f86
for example, the ROM selector created a new thumbnail animation with the AUTO spec when it's more appropriate to use the creation spec of the main emulation
391 lines
13 KiB
Go
391 lines
13 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 disassembly
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/jetsetilly/gopher2600/cartridgeloader"
|
|
"github.com/jetsetilly/gopher2600/disassembly/symbols"
|
|
"github.com/jetsetilly/gopher2600/environment"
|
|
"github.com/jetsetilly/gopher2600/hardware"
|
|
"github.com/jetsetilly/gopher2600/hardware/cpu"
|
|
"github.com/jetsetilly/gopher2600/hardware/cpu/execution"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cpubus"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
|
|
"github.com/jetsetilly/gopher2600/hardware/television"
|
|
)
|
|
|
|
// Disassembly represents the annotated disassembly of a 6507 binary.
|
|
type Disassembly struct {
|
|
Prefs *Preferences
|
|
|
|
// reference to running hardware. all access to VCS sub-systems to be done
|
|
// via this pointer. this facilitates the rewind system which would
|
|
// otherwise cause stale pointers to misleading information.
|
|
vcs *hardware.VCS
|
|
|
|
// symbols used to format disassembly output
|
|
Sym symbols.Symbols
|
|
|
|
// disasmEntries entries. use BorrowDisasm() for goroutines other than the
|
|
// emulation goroutine
|
|
disasmEntries DisasmEntries
|
|
|
|
// critical sectioning to protect disasmEntries
|
|
crit sync.Mutex
|
|
}
|
|
|
|
// DisasmEntries contains the individual disassembled entries of the current ROM.
|
|
type DisasmEntries struct {
|
|
// indexed by bank and address. address should be masked with memorymap.CartridgeBits before access
|
|
Entries [][]*Entry
|
|
}
|
|
|
|
// NewDisassembly is the preferred method of initialisation for the Disassembly
|
|
// type.
|
|
//
|
|
// Also returns a reference to the disassembly's symbol table. This reference
|
|
// will never change over the course of the lifetime of the Disassembly type
|
|
// itself. ie. the returned reference is safe to use after calls to
|
|
// FromMemory() or FromCartridge().
|
|
func NewDisassembly(vcs *hardware.VCS) (*Disassembly, *symbols.Symbols, error) {
|
|
dsm := &Disassembly{vcs: vcs}
|
|
|
|
var err error
|
|
|
|
dsm.Prefs, err = newPreferences(dsm)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
return dsm, &dsm.Sym, nil
|
|
}
|
|
|
|
const disassemblyLabel = environment.Label("disassembly")
|
|
|
|
// FromCartridge initialises a new partial emulation and returns a disassembly
|
|
// from the supplied cartridge filename. Useful for one-shot disassemblies,
|
|
// like the gopher2600 "disasm" mode.
|
|
func FromCartridge(cartload cartridgeloader.Loader) (*Disassembly, error) {
|
|
var err error
|
|
|
|
tv, err := television.NewTelevision("AUTO")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
vcs, err := hardware.NewVCS(disassemblyLabel, tv, nil, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
err = vcs.AttachCartridge(cartload, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
dsm, _, err := NewDisassembly(vcs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
// ignore errors caused by loading of symbols table - we always get a
|
|
// standard symbols table even in the event of an error
|
|
err = dsm.Sym.ReadDASMSymbolsFile(vcs.Mem.Cart)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
// do disassembly
|
|
err = dsm.FromMemory()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
return dsm, nil
|
|
}
|
|
|
|
// FromMemory disassembles an existing instance of cartridge memory using a
|
|
// cpu with no flow control. Unlike the FromCartridge() function this function
|
|
// requires an existing instance of Disassembly.
|
|
//
|
|
// Disassembly will start/assume the cartridge is in the correct starting bank.
|
|
func (dsm *Disassembly) FromMemory() error {
|
|
// unlocking manually before we call the disassmeble() function. this means
|
|
// we have to be careful to manually unlock before returning an error.
|
|
dsm.crit.Lock()
|
|
|
|
// create new memory
|
|
copiedBanks, err := dsm.vcs.Mem.Cart.CopyBanks()
|
|
if err != nil {
|
|
dsm.crit.Unlock()
|
|
return fmt.Errorf("disassembly: %w", err)
|
|
}
|
|
|
|
startingBank := dsm.vcs.Mem.Cart.GetBank(cpubus.Reset).Number
|
|
|
|
mem := newDisasmMemory(startingBank, copiedBanks)
|
|
if mem == nil {
|
|
dsm.crit.Unlock()
|
|
return fmt.Errorf("disassembly: %s", "could not create memory for disassembly")
|
|
}
|
|
|
|
// read symbols file
|
|
err = dsm.Sym.ReadDASMSymbolsFile(dsm.vcs.Mem.Cart)
|
|
if err != nil {
|
|
dsm.crit.Unlock()
|
|
return err
|
|
}
|
|
|
|
// allocate memory for disassembly. the GUI may find itself trying to
|
|
// iterate through disassembly at the same time as we're doing this.
|
|
dsm.disasmEntries.Entries = make([][]*Entry, dsm.vcs.Mem.Cart.NumBanks())
|
|
for b := 0; b < len(dsm.disasmEntries.Entries); b++ {
|
|
dsm.disasmEntries.Entries[b] = make([]*Entry, memorymap.CartridgeBits+1)
|
|
}
|
|
|
|
// exit early if cartridge memory self reports as being ejected
|
|
if dsm.vcs.Mem.Cart.IsEjected() {
|
|
dsm.crit.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// create a new NoFlowControl CPU to help disassemble memory
|
|
mc := cpu.NewCPU(nil, mem)
|
|
mc.NoFlowControl = true
|
|
|
|
dsm.crit.Unlock()
|
|
// end of critical section
|
|
|
|
// disassemble cartridge binary
|
|
return dsm.disassemble(mc, mem)
|
|
}
|
|
|
|
// GetEntryByAddress returns the disassembly entry at the specified
|
|
// bank/address. a returned value of nil indicates the entry is not in the
|
|
// cartridge; this will usually mean the address is in main VCS RAM.
|
|
//
|
|
// also returns whether cartridge is currently working from another source
|
|
// meaning that the disassembly entry might not be reliable.
|
|
func (dsm *Disassembly) GetEntryByAddress(address uint16) *Entry {
|
|
bank := dsm.vcs.Mem.Cart.GetBank(address)
|
|
|
|
if bank.NonCart {
|
|
// !!TODO: attempt to decode instructions not in cartridge
|
|
// when implemented, ammend comment for the STEP OVER command
|
|
return nil
|
|
}
|
|
|
|
return dsm.disasmEntries.Entries[bank.Number][address&memorymap.CartridgeBits]
|
|
}
|
|
|
|
// ExecutedEntry should be called after execution of a CPU instruction. In many
|
|
// instances it behaves the same as FormatResult with an EntryLevel of
|
|
// EntryLevelExecuted. Those intances are:
|
|
//
|
|
// - a coprocessor is executing (and is interfering with what is being executed)
|
|
// - the instruction being disassembled was retrieved from a non-Cartridge address
|
|
// - the instruction is from an unknown bank
|
|
//
|
|
// ExecutedEntry will update the disassembly corresponding to the result. If
|
|
// there is no existing entry, a new entry is added and a messsage is logged
|
|
// saying that there has been a "late decoding" - this suggests a flaw in the
|
|
// decoding process.
|
|
//
|
|
// checkNextAddr should be false if the result does no represent a completed
|
|
// instruction. in other words, if the instruction has only partially completed
|
|
func (dsm *Disassembly) ExecutedEntry(bank mapper.BankInfo, result execution.Result, checkNextAddr bool, nextAddr uint16) *Entry {
|
|
e := dsm.FormatResult(bank, result, EntryLevelExecuted)
|
|
|
|
// if co-processor is executing then whatever has been executed by the 6507
|
|
// will not relate to the permanent disassembly. format the result and
|
|
// return
|
|
//
|
|
// (in fact, there's a possible optimisation here where if we know the
|
|
// co-processor/mapper being used we can just return a predefined NOP
|
|
// disassembly)
|
|
if bank.ExecutingCoprocessor {
|
|
return e
|
|
}
|
|
|
|
// if executed entry is in non-cartridge space then we just format the
|
|
// result and return it. there's nothing else we can really do - there's no
|
|
// point caching it anywhere
|
|
if bank.NonCart {
|
|
return e
|
|
}
|
|
|
|
// similarly, if bank number is outside the banks we've already decoded
|
|
// then format the result and return
|
|
//
|
|
// I'm not sure when this would apply. maybe it's just a belt-and-braces
|
|
// check. there's no comment to say why this condition was added so leave
|
|
// it for now
|
|
if bank.Number >= len(dsm.disasmEntries.Entries) {
|
|
return e
|
|
}
|
|
|
|
// updating an entry can happen at the same time as iteration which is
|
|
// probably being run from a different goroutine. acknowledge the critical
|
|
// section
|
|
dsm.crit.Lock()
|
|
defer dsm.crit.Unlock()
|
|
|
|
// add/update entry to disassembly
|
|
idx := result.Address & memorymap.CartridgeBits
|
|
o := dsm.disasmEntries.Entries[bank.Number][idx]
|
|
if o != nil && o.Result.Final {
|
|
e.updateExecutionEntry(result)
|
|
}
|
|
dsm.disasmEntries.Entries[bank.Number][idx] = e
|
|
|
|
// bless next entry in case it was missed by the original decoding. there's
|
|
// no guarantee that the bank for the next address will be the same as the
|
|
// current bank, so we have to call the GetBank() function.
|
|
if checkNextAddr && result.Final {
|
|
bank = dsm.vcs.Mem.Cart.GetBank(nextAddr)
|
|
idx := nextAddr & memorymap.CartridgeBits
|
|
ne := dsm.disasmEntries.Entries[bank.Number][idx]
|
|
if ne == nil {
|
|
dsm.disasmEntries.Entries[bank.Number][idx] = dsm.FormatResult(bank, execution.Result{
|
|
Address: nextAddr,
|
|
}, EntryLevelBlessed)
|
|
} else if ne.Level < EntryLevelBlessed {
|
|
ne.Level = EntryLevelBlessed
|
|
}
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
// FormatResult creates an Entry for supplied result/bank. It will be assigned
|
|
// the specified EntryLevel.
|
|
//
|
|
// If EntryLevel is EntryLevelExecuted then the disassembly will be updated but
|
|
// only if result.Final is true.
|
|
func (dsm *Disassembly) FormatResult(bank mapper.BankInfo, result execution.Result, level EntryLevel) *Entry {
|
|
e := &Entry{
|
|
dsm: dsm,
|
|
Result: result,
|
|
Level: level,
|
|
Bank: bank.Number,
|
|
Label: Label{
|
|
dsm: dsm,
|
|
result: result,
|
|
bank: bank,
|
|
},
|
|
Operand: Operand{
|
|
dsm: dsm,
|
|
result: result,
|
|
bank: bank,
|
|
},
|
|
}
|
|
|
|
// address of instruction
|
|
e.Address = fmt.Sprintf("$%04x", result.Address)
|
|
|
|
// if definition is nil then set the operator field to ??? and return with no further formatting
|
|
if result.Defn == nil {
|
|
e.Operator = "???"
|
|
return e
|
|
}
|
|
|
|
// operator of instruction
|
|
e.Operator = result.Defn.Operator.String()
|
|
|
|
// bytecode and operand string is assembled depending on the number of
|
|
// expected bytes (result.Defn.Bytes) and the number of bytes read so far
|
|
// (result.ByteCount).
|
|
//
|
|
// the panics cover situations that should never exists. if result
|
|
// validation is active then the panic situations will have been caught
|
|
// then. if validation is not running then the code could theoretically
|
|
// panic but that's okay, they should have been caught in testing.
|
|
switch result.Defn.Bytes {
|
|
case 3:
|
|
switch result.ByteCount {
|
|
case 3:
|
|
operand := result.InstructionData
|
|
e.Operand.partial = fmt.Sprintf("$%04x", operand)
|
|
e.Bytecode = fmt.Sprintf("%02x %02x %02x", result.Defn.OpCode, operand&0x00ff, operand&0xff00>>8)
|
|
case 2:
|
|
operand := result.InstructionData
|
|
e.Operand.partial = fmt.Sprintf("$??%02x", result.InstructionData)
|
|
e.Bytecode = fmt.Sprintf("%02x %02x ?? ", result.Defn.OpCode, operand&0x00ff)
|
|
case 1:
|
|
e.Operand.partial = "$????"
|
|
e.Bytecode = fmt.Sprintf("%02x ?? ??", result.Defn.OpCode)
|
|
case 0:
|
|
panic("this makes no sense. we must have read at least one byte to know how many bytes to expect")
|
|
default:
|
|
panic("we should not be able to read more bytes than the expected number (expected 3)")
|
|
}
|
|
case 2:
|
|
switch result.ByteCount {
|
|
case 2:
|
|
operand := result.InstructionData
|
|
e.Operand.partial = fmt.Sprintf("$%02x", operand)
|
|
e.Bytecode = fmt.Sprintf("%02x %02x", result.Defn.OpCode, operand&0x00ff)
|
|
case 1:
|
|
e.Operand.partial = "$??"
|
|
e.Bytecode = fmt.Sprintf("%02x ??", result.Defn.OpCode)
|
|
case 0:
|
|
panic("this makes no sense. we must have read at least one byte to know how many bytes to expect")
|
|
default:
|
|
panic("we should not be able to read more bytes than the expected number (expected 2)")
|
|
}
|
|
case 1:
|
|
switch result.ByteCount {
|
|
case 1:
|
|
e.Bytecode = fmt.Sprintf("%02x", result.Defn.OpCode)
|
|
case 0:
|
|
panic("this makes no sense. we must have read at least one byte to know how many bytes to expect")
|
|
default:
|
|
panic("we should not be able to read more bytes than the expected number (expected 1)")
|
|
}
|
|
case 0:
|
|
panic("instructions of zero bytes is not possible")
|
|
default:
|
|
panic("instructions of more than 3 bytes is not possible")
|
|
}
|
|
e.Bytecode = strings.TrimSpace(e.Bytecode)
|
|
|
|
// decorate operand with addressing mode indicators. this decorates the
|
|
// non-symbolic operand. we also call the decorate function from the
|
|
// Operand() function when a symbol has been found
|
|
e.Operand.partial = addrModeDecoration(e.Operand.partial, e.Result.Defn.AddressingMode)
|
|
|
|
return e
|
|
}
|
|
|
|
// BorrowDisasm will lock the DisasmEntries structure for the durction of the
|
|
// supplied function, which will be executed with the disasm structure as an
|
|
// argument.
|
|
//
|
|
// Function will be executed with a nil argument if disassembly is not valid.
|
|
//
|
|
// Should not be called from the emulation goroutine.
|
|
func (dsm *Disassembly) BorrowDisasm(f func(*DisasmEntries)) {
|
|
dsm.crit.Lock()
|
|
defer dsm.crit.Unlock()
|
|
|
|
f(&dsm.disasmEntries)
|
|
}
|