mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-06-02 03:58:02 -04:00
cd2a00d4ba
the logger.Permission interface indicates whether the environment making the logging request is allowed to create new log entries. the environment.Environment type satisifies the Permission interface logger.Allow is provided as a convienient way of indicating the the log entry should always be created
381 lines
13 KiB
Go
381 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(logger.Allow, "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(logger.Allow, "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(logger.Allow, "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
|
|
}
|