Gopher2600/disassembly/decode.go
JetSetIlly 3aa5885ebe removed curated pacakge. replaced with wrapped errors
curated package predated the standard errors package introduced in
go1.13

the standard package does a better job of what curated attempted to do

the change of package also gave me a opportunity to clean up the error
messages a little bit
2023-02-13 21:58:39 +00:00

380 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"
"github.com/jetsetilly/gopher2600/disassembly/symbols"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/cpu/instructions"
"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/logger"
)
func (dsm *Disassembly) disassemble(mc *cpu.CPU, mem *disasmMemory) error {
// basic decoding pass
err := dsm.decode(mc, mem)
if err == nil {
// bless those entries which we're reasonably sure are real instructions
err = dsm.bless(mc, mem)
if err != nil {
return err
}
// convert addresses to preferred mirror
dsm.setCartMirror()
}
return err
}
func (dsm *Disassembly) bless(mc *cpu.CPU, mem *disasmMemory) error {
// bless from reset address for every bank
for b := range dsm.disasmEntries.Entries {
// get reset address from starting bank, taking into account the
// possibility that bank is smalled thank 4096 bytes
resetVector := cpubus.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(b, resetAddr, true)
}
}
moreBlessing := true
attempts := 0
for moreBlessing {
moreBlessing = false
// make sure we're not going bezerk. a limit of 10 is probably too high
attempts++
if attempts > 10 {
break
}
// loop through every bank in the cartridge and collate a list of blessings
// for the bank
for b := range dsm.disasmEntries.Entries {
for i := range dsm.disasmEntries.Entries[b] {
e := dsm.disasmEntries.Entries[b][i]
// ignore any non-blessed entry
if e.Level != EntryLevelBlessed {
continue
}
// if instruction is a JMP or JSR then check the target address and
// which banks it can feasibly be in. add every matching bank to
// the blessings list
//
// an example of interbank JMP/JSR jumping is the beginning of the
// HeMan ROM. From bank 7 the following jumps to bank 5.
//
// $fa03 JMP $f7e8
if e.Result.Defn.Operator == instructions.Jmp || e.Result.Defn.Operator == instructions.Jsr {
jmpAddress, area := memorymap.MapAddress(e.Result.InstructionData, true)
if area == memorymap.Cartridge {
l := jmpTargets(mem.banks, jmpAddress)
for _, jb := range l {
if dsm.blessSequence(jb, jmpAddress, false) {
if dsm.blessSequence(jb, jmpAddress, true) {
moreBlessing = true
}
}
// add label to the symbols table
dsm.Sym.AddLabelAuto(jb, jmpAddress)
}
}
} else {
// if instruction is a branch then add the address of the successful branch
// to the blessing list. assumption here is that a branch will never branch
// to another bank
// !!TODO: find practical examples of interbank branch jumping
if e.Result.Defn.IsBranch() {
mc.PC.Load(e.Result.Address)
mc.PC.Add(uint16(e.Result.Defn.Bytes))
operand := e.Result.InstructionData
if operand&0x0080 == 0x0080 {
operand |= 0xff00
}
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) {
moreBlessing = true
}
}
// add label to the symbols table
dsm.Sym.AddLabelAuto(b, pcAddress)
}
}
}
}
}
}
// correct DASM symbols. DASM sometimes records that a label is at a
// location adjacent to were it should be. I've only ever seen it such the
// label is one address after where it should be but other variations may
// exists
//
// the loop below works by:
// 1) looking for an automatically labelled address (AUTO) that has been blessed
// 2) if the address following that label has a DASM symbol then
// 3) remove the AUTO label
// and
// 4) move the DASM label to the previously labelled with the AUTO label
for b := range dsm.disasmEntries.Entries {
for i := range dsm.disasmEntries.Entries[b] {
if dsm.disasmEntries.Entries[b][i].Level >= EntryLevelBlessed {
addr := dsm.disasmEntries.Entries[b][i].Result.Address
if sym, ok := dsm.Sym.GetLabel(b, addr); ok && sym.Source == symbols.SourceAuto {
if i < len(dsm.disasmEntries.Entries[b]) {
postAddr := dsm.disasmEntries.Entries[b][i+1].Result.Address
if postSym, ok := dsm.Sym.GetLabel(b, postAddr); ok && postSym.Source == symbols.SourceDASM {
_ = dsm.Sym.RemoveLabel(symbols.SourceAuto, b, addr)
_ = dsm.Sym.RemoveLabel(symbols.SourceDASM, b, postAddr)
dsm.Sym.AddLabel(symbols.SourceDASM, b, addr, postSym.Symbol)
}
}
}
}
}
}
// remove any auto-labels that have been added to entries that have not
// been blessed (these labels were added speculatively).
for b := range dsm.disasmEntries.Entries {
for i := range dsm.disasmEntries.Entries[b] {
if dsm.disasmEntries.Entries[b][i].Level < EntryLevelBlessed {
_ = dsm.Sym.RemoveLabel(symbols.SourceAuto, b, dsm.disasmEntries.Entries[b][i].Result.Address)
}
}
}
return nil
}
// jmpTargets returns a list of banks that a JMP or JSR target address may
// feasibly be in.
func jmpTargets(copiedBanks []mapper.BankContent, jmpAddress uint16) []int {
l := make([]int, 0, len(copiedBanks))
// only the significant bits of the jmp and origin addresses are compared.
// this is the easiest way of handling different cartridge mirrors
jmpAddress &= memorymap.CartridgeBits
// find banks which can be mapped to cover the supplied jmpAddress
for _, b := range copiedBanks {
for _, o := range b.Origins {
o &= memorymap.CartridgeBits
if jmpAddress >= o && jmpAddress <= o+uint16(len(b.Data)) {
l = append(l, b.Number)
break
}
}
}
return l
}
// returns false if sequence is false. generally, the function should be called
// twice for a bank/addr. once with commit set to false and then if the return
// value is true, called again with commit set to true.
//
// if commit is true without the sequence being sure, a log entry will be made
// in the event of mistake
//
// it does not matter if addr is a normalised or unormalised address.
func (dsm *Disassembly) blessSequence(bank int, addr uint16, commit bool) bool {
// mask address into indexable range
a := addr & memorymap.CartridgeBits
// blessing 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()
hasCommitted := false
// examine every entry from the starting point a. next entry determined in
// program counter style (ie. address plus instruction byte count)
//
// sequence will stop if:
// . an unknown opcode has been encountered
// . if the end of cartridge data has been reached
// . there is already blessed instruction between this and the next entry
// . a flow control instruction is encountered (this is normal and expected)
// . an instruction looks like fill characters
//
// the following will stop the blessing but indicate that the blessing is
// "correct" if the following have been encountered:
//
// . an RTS instruction
// . a branch instruction
// . an interrupt
for a < uint16(len(dsm.disasmEntries.Entries[bank])) {
instruction := dsm.disasmEntries.Entries[bank][a]
// end run if entry has already been blessed
if instruction.Level == EntryLevelBlessed {
// this is okay and expected so return true to indicate that the
// blessing should continue
return true
}
next := a + uint16(instruction.Result.ByteCount)
// break if address has looped around
if next > next&memorymap.CartridgeBits {
if hasCommitted {
logger.Logf("disassembly", "blessSequence has blessed an instruction in a false sequence. discovered at bank %d: %s", bank, instruction.String())
}
return false
}
// if an entry between this entry and the next has already been
// blessed then this track is probably not correct.
for i := a + 1; i < next; i++ {
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
}
}
// do not bless decoded instructions that look like fill characters
if instruction.Result.Defn.OpCode == 0xff && instruction.Result.InstructionData == 0xffff {
if hasCommitted {
logger.Logf("disassembly", "blessSequence has blessed an instruction in a false sequence. discovered at bank %d: %s", bank, instruction.String())
}
return false
}
// promote the entry
if commit {
hasCommitted = true
instruction.Level = EntryLevelBlessed
}
// end the blessing sequence if we encountered an instruction that breaks
// the flow with no possibility of resumption. In practical terms this
// means JMP, RTS and Interrupt instructions.
if instruction.Operator == "jmp" || instruction.Operator == "rts" ||
instruction.Result.Defn.Effect == instructions.Interrupt {
return true
}
// next address
a = next
}
// reached end of the bank without encountering any other halt condition
return false
}
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 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
// "segments" of cartridge memory
for _, origin := range bank.Origins {
// make sure origin address is rooted correctly. we'll convert all
// addresses to the preferred mirror at the end of the disassembly
mem.origin = (origin & memorymap.CartridgeBits) | memorymap.OriginCart
// the memtop for the bank
memtop := origin + uint16(len(bank.Data)) - 1
// reset CPU for each bank/origin
mc.Reset()
// loop over entire address space for cartridge. even then bank
// sizes are smaller than the address space it makes things easier
// later if we put a valid entry at every index. entries outside of
// the bank space will have level == EntryLevelUnused
for address := memorymap.OriginCart; address <= memorymap.MemtopCart; address++ {
// continue if entry has already been decoded
e := dsm.disasmEntries.Entries[bank.Number][address&memorymap.CartridgeBits]
if e != nil && e.Level > EntryLevelUnmappable {
continue
}
// decide whether address is mappable or not. even if it isn't, we'll still go
// through the decoding process so that we have a usuable entry at all points
// in the disassembly. this simplifies future iterations.
entryLevel := EntryLevelUnmappable
if address >= origin && address <= memtop {
entryLevel = EntryLevelDecoded
}
// execute instruction at address
mc.PC.Load(address)
err := mc.ExecuteInstruction(cpu.NilCycleCallback)
if err != nil {
return fmt.Errorf("decode: %w", err)
}
// error on invalid instruction execution
if err = mc.LastResult.IsValid(); err != nil {
return fmt.Errorf("decode: %w", err)
}
// add entry to disassembly
ent := dsm.FormatResult(mapper.BankInfo{Number: bank.Number}, mc.LastResult, entryLevel)
dsm.disasmEntries.Entries[bank.Number][address&memorymap.CartridgeBits] = ent
}
}
}
// sanity checks
for b := range dsm.disasmEntries.Entries {
for _, a := range dsm.disasmEntries.Entries[b] {
if a == nil {
return fmt.Errorf("decode: not every address has been decoded")
}
if a.Level == EntryLevelUnmappable {
if a.Result.Defn.OpCode != 0x00 {
return fmt.Errorf("decode: an unmappable bank address [%#04x bank %d] has a non 0x00 opcode", a.Result.Address, b)
}
}
}
}
return nil
}