improved disassembly of non-4k bank sizes

clarified disassembly memory model

corrected decision making in blessSequence() function

there will still be instances when executed entries won't/can't be detected statically

fixes #14
This commit is contained in:
JetSetIlly 2021-06-30 15:03:52 +01:00
parent b1bace8ebf
commit af1fc91bbb
4 changed files with 96 additions and 49 deletions

View file

@ -26,19 +26,14 @@ import (
)
func (dsm *Disassembly) disassemble(mc *cpu.CPU, mem *disasmMemory) error {
copiedBanks, err := dsm.vcs.Mem.Cart.CopyBanks()
if err != nil {
return curated.Errorf("decode: %v", err)
}
// basic decoding pass
err = dsm.decode(mc, mem, copiedBanks)
err := dsm.decode(mc, mem)
if err != nil {
return err
}
// bless those entries which we're reasonably sure are real instructions
err = dsm.bless(mc, copiedBanks)
err = dsm.bless(mc, mem)
if err != nil {
return err
}
@ -49,24 +44,19 @@ func (dsm *Disassembly) disassemble(mc *cpu.CPU, mem *disasmMemory) error {
return nil
}
func (dsm *Disassembly) bless(mc *cpu.CPU, copiedBanks []mapper.BankContent) error {
// we can be sure the machine reset address is a starting point for a blessing
err := mc.LoadPCIndirect(addresses.Reset)
if err != nil {
return err
}
resetAddress, resetArea := memorymap.MapAddress(mc.PC.Value(), true)
if resetArea != memorymap.Cartridge {
logger.Log("disassembly", "cartridge does not reset to an address in the cartridge area.")
}
// bless from reset address for every bank. note that we do not test to see
// if the blessSequence is "correct" before committal because execution
// from the reset address is likely
func (dsm *Disassembly) bless(mc *cpu.CPU, mem *disasmMemory) error {
// bless from reset address for every bank
for b := range dsm.entries {
mc.PC.Load(resetAddress)
_ = dsm.blessSequence(b, mc.PC.Value(), true)
// get reset address from starting bank, taking into account the
// possibility that bank is smalled thank 4096 bytes
resetVector := addresses.Reset & (uint16(len(mem.banks[b].Data) - 1))
resetAddr := (uint16(mem.banks[b].Data[resetVector+1]) << 8) | uint16(mem.banks[b].Data[resetVector])
// make sure reset address is valid
_, area := memorymap.MapAddress(resetAddr, true)
if area == memorymap.Cartridge {
_ = dsm.blessSequence(mem.startingBank, resetAddr, true)
}
}
moreBlessing := true
@ -75,10 +65,10 @@ func (dsm *Disassembly) bless(mc *cpu.CPU, copiedBanks []mapper.BankContent) err
for moreBlessing {
moreBlessing = false
// make sure we're not going bezerk. a limit of 100 is probably far too high
// make sure we're not going bezerk. a limit of 10 is probably too high
attempts++
if attempts > 100 {
return curated.Errorf("bless: cannot discover a finite bless path through the program")
if attempts > 10 {
break
}
// loop through every bank in the cartridge and collate a list of blessings
@ -104,7 +94,7 @@ func (dsm *Disassembly) bless(mc *cpu.CPU, copiedBanks []mapper.BankContent) err
jmpAddress, area := memorymap.MapAddress(e.Result.InstructionData, true)
if area == memorymap.Cartridge {
l := jmpTargets(copiedBanks, jmpAddress)
l := jmpTargets(mem.banks, jmpAddress)
for _, jb := range l {
if dsm.blessSequence(jb, jmpAddress, false) {
@ -132,6 +122,7 @@ func (dsm *Disassembly) bless(mc *cpu.CPU, copiedBanks []mapper.BankContent) err
mc.PC.Add(operand)
pcAddress, area := memorymap.MapAddress(mc.PC.Value(), true)
if area == memorymap.Cartridge {
if dsm.blessSequence(b, pcAddress, false) {
if dsm.blessSequence(b, pcAddress, true) {
@ -229,10 +220,9 @@ func (dsm *Disassembly) blessSequence(bank int, addr uint16, commit bool) bool {
// end run if entry has already been blessed
if instruction.Level == EntryLevelBlessed {
if hasCommitted {
logger.Logf("disassembly", "blessSequence has blessed an instruction in a false sequence. discovered at bank %d: %s", bank, instruction.String())
}
return false
// this is okay and expected so return true to indicate that the
// blessing should continue
return true
}
// if operator is unknown than end the sequence.
@ -276,6 +266,7 @@ func (dsm *Disassembly) blessSequence(bank int, addr uint16, commit bool) bool {
// promote the entry
if commit {
hasCommitted = true
instruction.Level = EntryLevelBlessed
}
@ -295,15 +286,14 @@ func (dsm *Disassembly) blessSequence(bank int, addr uint16, commit bool) bool {
return false
}
func (dsm *Disassembly) decode(mc *cpu.CPU, mem *disasmMemory, copiedBanks []mapper.BankContent) error {
func (dsm *Disassembly) decode(mc *cpu.CPU, mem *disasmMemory) error {
// decoding 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()
for _, bank := range copiedBanks {
// point memory implementation to iterated bank
mem.bank = bank
for _, bank := range mem.banks {
mem.currentBank = bank.Number
// try each bank in each possible segment - banks that are smaller than
// the cartridge addressing range can often be mapped into different

View file

@ -27,6 +27,7 @@ import (
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/cpu/execution"
"github.com/jetsetilly/gopher2600/hardware/memory/addresses"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/hardware/television"
@ -128,16 +129,31 @@ func FromCartridge(cartload cartridgeloader.Loader) (*Disassembly, error) {
// 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
// requires an existing instance of Disassembly.
//
// cartridge will finish in its initialised state.
// Disassembly will start/assume the cartridge is in the correct starting bank.
func (dsm *Disassembly) FromMemory() error {
dsm.crit.Lock()
// 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 curated.Errorf("disassembly: %v", err)
}
startingBank := dsm.vcs.Mem.Cart.GetBank(addresses.Reset).Number
mem := newDisasmMemory(startingBank, copiedBanks)
if mem == nil {
dsm.crit.Unlock()
return curated.Errorf("disassembly: %s", "could not create memory for disassembly")
}
// read symbols file
err := dsm.Sym.ReadSymbolsFile(dsm.vcs.Mem.Cart)
err = dsm.Sym.ReadSymbolsFile(dsm.vcs.Mem.Cart)
if err != nil {
dsm.crit.Unlock()
return err
@ -156,9 +172,6 @@ func (dsm *Disassembly) FromMemory() error {
return nil
}
// create new memory
mem := &disasmMemory{}
// create a new NoFlowControl CPU to help disassemble memory
mc := cpu.NewCPU(nil, mem)
mc.NoFlowControl = true

View file

@ -47,4 +47,13 @@
// to the Symbol tables. This reference will be valid throughout the lifetime
// of the Disassembly instance and will "survive" calls to the FromMemory() and
// FromCartridge() functions.
//
// Segmented Cartridges
//
// The disassembly package treats small bank sized (those less than 4k) by
// performing the disassembly with the cartridge rooted at each origin point -
// in each possible segment allowed by the mapper.
//
// Origin information is held in the mappers.BankContent type returned by the
// cartridge.CopyBanks() function.
package disassembly

View file

@ -23,22 +23,57 @@ import (
// disasmMemory is a simplified memory model that allows the emulated CPU to
// read cartridge memory without touching the actual cartridge.
type disasmMemory struct {
// if bank is not nil then the bank is read directly
bank mapper.BankContent
// the bank which the cartridge starts on
startingBank int
// current bank to index the banks array
currentBank int
banks []mapper.BankContent
// the current origin for the mapped bank
origin uint16
}
func newDisasmMemory(startingBank int, copiedBanks []mapper.BankContent) *disasmMemory {
dismem := &disasmMemory{
startingBank: startingBank,
currentBank: startingBank,
banks: copiedBanks,
}
return dismem
}
func (dismem *disasmMemory) reset() {
dismem.currentBank = dismem.startingBank
dismem.origin = dismem.banks[dismem.currentBank].Origins[0]
}
func (dismem *disasmMemory) Read(address uint16) (uint8, error) {
// map address
address, area := memorymap.MapAddress(address, true)
if area == memorymap.Cartridge {
// bank field is set so we bypass the cartridge mapper's usual read
// logic and access the bank directly
// bring address into range. if it's still outside of range return a
// zero byte (with no error)
//
// the alternative to this strategy is to use a mask based on the
// actual length of the bank. in most cases this will be the same as
// memorymap.CartridgeBits but some cartridge mappers use banks that
// are smaller than that:
//
// address = address & uint16(len(dismem.banks[0].Data)-1)
//
// the problem with this is that the Read() function will return a byte
// which may be misleading to the disassembler. using the strategy
// below a value of zero is returned.
//
// no error is returned, even though it might seem we should, because
// it would only cause the CPU to halt mid-instruction, which would
// only cause other complications.
address = (address - dismem.origin) & memorymap.CartridgeBits
if address >= uint16(len(dismem.bank.Data)) {
if address >= uint16(len(dismem.banks[dismem.currentBank].Data)) {
return 0, nil
}
return dismem.bank.Data[address], nil
return dismem.banks[dismem.currentBank].Data[address], nil
}
// address outside of cartridge range return nothing