- result sub-package renamed to execution
    - renamed Instruction type therein to Instruction

o disassembly
    - reworked structure of pacakge
    - better grep. scope of grep can now be specified
    - display sub-package added
    - disassemblies now store instance of display.DisasmInstruction instead of a formatted string

o debugger
    - ammende GREP to support new disassembly features
This commit is contained in:
steve 2019-12-23 08:54:02 +00:00
parent 0f5a258482
commit 6616bc2b88
25 changed files with 510 additions and 509 deletions

View file

@ -1,15 +1,16 @@
package debugger
import (
"bytes"
"fmt"
"gopher2600/cartridgeloader"
"gopher2600/debugger/script"
"gopher2600/debugger/terminal"
"gopher2600/debugger/terminal/commandline"
"gopher2600/disassembly"
"gopher2600/errors"
"gopher2600/gui"
"gopher2600/hardware/cpu/registers"
"gopher2600/hardware/cpu/result"
"gopher2600/hardware/memory/addresses"
"gopher2600/hardware/memory/memorymap"
"gopher2600/hardware/riot/input"
@ -79,10 +80,10 @@ var commandTemplate = []string{
cmdCartridge + " (ANALYSIS|BANK %N)",
cmdClear + " [BREAKS|TRAPS|WATCHES|ALL]",
cmdDebuggerState,
cmdDisassembly,
cmdDisassembly + " (BYTECODE)",
cmdDisplay + " (ON|OFF|DEBUG (ON|OFF)|SCALE [%P]|ALT (ON|OFF)|OVERLAY (ON|OFF))", // see notes
cmdDrop + " [BREAK|TRAP|WATCH] %N",
cmdGrep + " %S",
cmdGrep + " (MNEMONIC|OPERAND) %S",
cmdHexLoad + " %N %N {%N}",
cmdInsert + " %F",
cmdLast + " (DEFN|BYTECODE)",
@ -319,10 +320,6 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
defer func() {
dbg.scriptScribe.EndPlayback()
}()
// !!TODO: provide a recording option to allow insertion of
// the actual script commands rather than the call to the
// script itself
}
err = dbg.inputLoop(scr, false)
@ -332,12 +329,29 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
}
case cmdDisassembly:
dbg.disasm.Dump(dbg.printStyle(terminal.StyleFeedback))
option, _ := tokens.Get()
bytecode := option == "BYTECODE"
s := &bytes.Buffer{}
dbg.disasm.Write(s, bytecode)
dbg.print(terminal.StyleFeedback, s.String())
case cmdGrep:
scope := disassembly.GrepAll
s, _ := tokens.Get()
switch strings.ToUpper(s) {
case "MNEMONIC":
scope = disassembly.GrepMnemonic
case "OPERAND":
scope = disassembly.GrepOperand
default:
tokens.Unget()
}
search, _ := tokens.Get()
output := strings.Builder{}
dbg.disasm.Grep(&output, search, false, 3)
dbg.disasm.Grep(&output, scope, search, false)
if output.Len() == 0 {
dbg.print(terminal.StyleError, "%s not found in disassembly", search)
} else {
@ -590,35 +604,39 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
return doNothing, nil
case cmdLast:
if dbg.vcs.CPU.LastResult.Defn == nil {
// special condition for when LAST is called before any execution
// has taken place
dbg.print(terminal.StyleFeedback, "no instruction decoded yet")
s := strings.Builder{}
d, err := dbg.disasm.FormatResult(dbg.vcs.CPU.LastResult)
if err != nil {
return doNothing, err
}
option, ok := tokens.Get()
if ok {
switch strings.ToUpper(option) {
case "DEFN":
dbg.print(terminal.StyleFeedback, "%s", dbg.vcs.CPU.LastResult.Defn)
break
case "BYTECODE":
s.WriteString(fmt.Sprintf(dbg.disasm.Columns.Fmt.Bytecode, d.Bytecode))
}
}
s.WriteString(fmt.Sprintf(dbg.disasm.Columns.Fmt.Address, d.Address))
s.WriteString(" ")
s.WriteString(fmt.Sprintf(dbg.disasm.Columns.Fmt.Mnemonic, d.Mnemonic))
s.WriteString(" ")
s.WriteString(fmt.Sprintf(dbg.disasm.Columns.Fmt.Operand, d.Operand))
s.WriteString(" ")
s.WriteString(fmt.Sprintf(dbg.disasm.Columns.Fmt.Cycles, d.Cycles))
s.WriteString(" ")
s.WriteString(fmt.Sprintf(dbg.disasm.Columns.Fmt.Notes, d.Notes))
if dbg.vcs.CPU.LastResult.Final {
dbg.print(terminal.StyleCPUStep, s.String())
} else {
done := false
resultStyle := result.StyleExecution
option, ok := tokens.Get()
if ok {
switch strings.ToUpper(option) {
case "DEFN":
dbg.print(terminal.StyleFeedback, "%s", dbg.vcs.CPU.LastResult.Defn)
done = true
case "BYTECODE":
resultStyle = result.StyleDisasm
}
}
if !done {
var printTag terminal.Style
if dbg.vcs.CPU.LastResult.Final {
printTag = terminal.StyleCPUStep
} else {
printTag = terminal.StyleVideoStep
}
dbg.print(printTag, "%s", dbg.vcs.CPU.LastResult.GetString(dbg.disasm.Symtable, resultStyle))
}
dbg.print(terminal.StyleVideoStep, s.String())
}
case cmdMemMap:

View file

@ -14,15 +14,9 @@ import (
// function. output will be normalised and sent to the attached terminal as
// required.
func (dbg *Debugger) print(sty terminal.Style, s string, a ...interface{}) {
// resolve string placeholders and return if the resulting string is empty
if sty != terminal.StyleHelp {
s = fmt.Sprintf(s, a...)
if len(s) == 0 {
return
}
}
// trim *all* trailing newlines - UserPrint() will add newlines if required
// resolve string placeholders, remove all trailing newlines, and return if
// the resulting string is empty
s = fmt.Sprintf(s, a...)
s = strings.TrimRight(s, "\n")
if len(s) == 0 {
return

View file

@ -36,10 +36,14 @@ func (dbg *Debugger) buildPrompt(videoCycle bool) terminal.Prompt {
// prompt address doesn't seem to be pointing to the cartridge, prepare
// "non-cart" prompt
prompt.WriteString(fmt.Sprintf(" %#04x non-cart space ]", promptAddress))
} else if entry, ok := dbg.disasm.Get(promptBank, promptAddress); ok {
} else if d, ok := dbg.disasm.Get(promptBank, promptAddress); ok {
// because we're using the raw disassmebly the reported address
// in that disassembly may be misleading.
prompt.WriteString(fmt.Sprintf(" %#04x %s ]", promptAddress, entry.String()))
prompt.WriteString(fmt.Sprintf(" %#04x %s", promptAddress, d.Mnemonic))
if d.Operand != "" {
prompt.WriteString(fmt.Sprintf(" %s", d.Operand))
}
prompt.WriteString(" ]")
} else {
// incomplete disassembly, prepare "no disasm" prompt
prompt.WriteString(fmt.Sprintf(" %#04x (%d) no disasm ]", promptAddress, promptBank))

View file

@ -30,7 +30,6 @@ func (ct *ColorTerminal) TermPrint(style terminal.Style, s string, a ...interfac
ct.EasyTerm.TermPrint("* ")
case terminal.StyleHelp:
ct.EasyTerm.TermPrint(ansi.DimPens["white"])
ct.EasyTerm.TermPrint(" ")
case terminal.StyleFeedback:
ct.EasyTerm.TermPrint(ansi.DimPens["white"])
case terminal.StylePromptCPUStep:

View file

@ -33,7 +33,8 @@
// that we know have passed the validation. For example, using the above
// template, we can implement a switch very consisely:
//
// switch toks.Get() {
// option, _ := toks.Get()
// switch strings.ToUpper(option) {
// case "LIST:
// list()
// case "PRINT:

View file

@ -43,8 +43,6 @@ func (pt PlainTerminal) TermPrint(style terminal.Style, s string, a ...interface
switch style {
case terminal.StyleError:
s = fmt.Sprintf("* %s", s)
case terminal.StyleHelp:
s = fmt.Sprintf(" %s", s)
}
s = fmt.Sprintf(s, a...)

View file

@ -138,10 +138,7 @@ func (wtc *watches) parseWatch(tokens *commandline.Tokens, dbgmem *memoryDebug)
var event watchEvent
// read mode
mode, present := tokens.Get()
if !present {
return errors.New(errors.CommandError, "watch address required")
}
mode, _ := tokens.Get()
mode = strings.ToUpper(mode)
switch mode {
case "READ":

View file

@ -3,18 +3,19 @@ package disassembly
import (
"fmt"
"gopher2600/cartridgeloader"
"gopher2600/disassembly/display"
"gopher2600/errors"
"gopher2600/hardware/cpu"
"gopher2600/hardware/cpu/execution"
"gopher2600/hardware/memory/addresses"
"gopher2600/hardware/memory/cartridge"
"gopher2600/symbols"
"io"
"strings"
)
const disasmMask = 0x0fff
type bank [disasmMask + 1]Entry
type bank [disasmMask + 1]*display.Instruction
// Disassembly represents the annotated disassembly of a 6507 binary
type Disassembly struct {
@ -25,7 +26,7 @@ type Disassembly struct {
interrupts bool
forcedRTS bool
// symbols used to build disassembly output
// symbols used to format disassembly output
Symtable *symbols.Table
// linear is the decoding of every possible address in the cartridge
@ -34,6 +35,11 @@ type Disassembly struct {
// flow is the decoding of cartridge addresses that follow the flow from
// the start address
flow []bank
// formatting information for all entries in the flow disassembly.
// excluding the linear disassembly because false positives entries might
// upset the formatting.
Columns display.Columns
}
// Analysis returns a summary of anything interesting found during disassembly.
@ -45,27 +51,20 @@ func (dsm Disassembly) Analysis() string {
return s.String()
}
// Get returns the disassembled entry at the specified bank/address. This
// Get returns the disassembly at the specified bank/address
//
// function works best when the address definitely points to a valid
// instruction. This probably means during the execution of a the cartridge
// with proper flow control.
func (dsm Disassembly) Get(bank int, address uint16) (Entry, bool) {
entry := dsm.linear[bank][address&disasmMask]
return entry, entry.IsInstruction()
func (dsm Disassembly) Get(bank int, address uint16) (*display.Instruction, bool) {
col := dsm.linear[bank][address&disasmMask]
return col, col != nil
}
// Dump writes the entire disassembly to the write interface
func (dsm *Disassembly) Dump(output io.Writer) {
for bank := 0; bank < len(dsm.flow); bank++ {
output.Write([]byte(fmt.Sprintf("--- bank %d ---\n", bank)))
for i := range dsm.flow[bank] {
if entry := dsm.flow[bank][i]; entry.instructionDefinition != nil {
output.Write([]byte(entry.instruction))
output.Write([]byte("\n"))
}
}
}
// FormatResult is a wrapper for the display.Format() function using the
// current symbol table
func (dsm Disassembly) FormatResult(result execution.Result) (*display.Instruction, error) {
return display.Format(result, dsm.Symtable)
}
// FromCartridge initialises a new partial emulation and returns a

View file

@ -0,0 +1,63 @@
package display
import "fmt"
// Widths of DisasmInstuction entry fields.
type Widths struct {
Location int
Bytecode int
Address int
Mnemonic int
Operand int
Cycles int
Notes int
}
// Fmt strings for Instruction fields. For use with fmt.Printf() and fmt.Sprintf()
type Fmt struct {
Location string
Bytecode string
Address string
Mnemonic string
Operand string
Cycles string
Notes string
}
// Columns information for groups of Instructions
type Columns struct {
Widths Widths
Fmt Fmt
}
// Update width and formatting information
func (col *Columns) Update(d *Instruction) {
if len(d.Location) > col.Widths.Location {
col.Widths.Location = len(d.Location)
col.Fmt.Location = fmt.Sprintf("%%%ds", col.Widths.Location)
}
if len(d.Bytecode) > col.Widths.Bytecode {
col.Widths.Bytecode = len(d.Bytecode)
col.Fmt.Bytecode = fmt.Sprintf("%%%ds", col.Widths.Bytecode)
}
if len(d.Address) > col.Widths.Address {
col.Widths.Address = len(d.Address)
col.Fmt.Address = fmt.Sprintf("%%%ds", col.Widths.Address)
}
if len(d.Mnemonic) > col.Widths.Mnemonic {
col.Widths.Mnemonic = len(d.Mnemonic)
col.Fmt.Mnemonic = fmt.Sprintf("%%%ds", col.Widths.Mnemonic)
}
if len(d.Operand) > col.Widths.Operand {
col.Widths.Operand = len(d.Operand)
col.Fmt.Operand = fmt.Sprintf("%%%ds", col.Widths.Operand)
}
if len(d.Cycles) > col.Widths.Cycles {
col.Widths.Cycles = len(d.Cycles)
col.Fmt.Cycles = fmt.Sprintf("%%%ds", col.Widths.Cycles)
}
if len(d.Notes) > col.Widths.Notes {
col.Widths.Notes = len(d.Notes)
col.Fmt.Notes = fmt.Sprintf("%%%ds", col.Widths.Notes)
}
}

View file

@ -0,0 +1,174 @@
package display
import (
"fmt"
"gopher2600/hardware/cpu/execution"
"gopher2600/hardware/cpu/instructions"
"gopher2600/hardware/cpu/registers"
"gopher2600/symbols"
"strings"
)
// Instruction is the fully annotated, columnular representation of
// a instance of execution.Instruction
type Instruction struct {
Location string
Bytecode string
Address string
Mnemonic string
Operand string
Cycles string
Notes string
}
// Format execution.Result and create a new instance of Instruction
func Format(result execution.Result, symtable *symbols.Table) (*Instruction, error) {
if symtable == nil {
symtable = &symbols.Table{}
}
d := &Instruction{}
// if the operator hasn't been decoded yet then use placeholder strings in
// key columns
if result.Defn == nil {
d.Mnemonic = "???"
d.Operand = "?"
return d, nil
}
d.Address = fmt.Sprintf("0x%04x", result.Address)
if v, ok := symtable.Locations.Symbols[result.Address]; ok {
d.Location = v
}
d.Mnemonic = result.Defn.Mnemonic
// operands
var operand uint16
switch result.InstructionData.(type) {
case uint8:
operand = uint16(result.InstructionData.(uint8))
d.Operand = fmt.Sprintf("$%02x", operand)
case uint16:
operand = uint16(result.InstructionData.(uint16))
d.Operand = fmt.Sprintf("$%04x", operand)
case nil:
if result.Defn.Bytes == 2 {
d.Operand = "??"
} else if result.Defn.Bytes == 3 {
d.Operand = "????"
}
}
// Bytecode
if result.Final {
switch result.Defn.Bytes {
case 3:
d.Bytecode = fmt.Sprintf("%02x", operand&0xff00>>8)
fallthrough
case 2:
d.Bytecode = fmt.Sprintf("%02x %s", operand&0x00ff, d.Bytecode)
fallthrough
case 1:
d.Bytecode = fmt.Sprintf("%02x %s", result.Defn.OpCode, d.Bytecode)
default:
d.Bytecode = fmt.Sprintf("(%d bytes) %s", result.Defn.Bytes, d.Bytecode)
}
d.Bytecode = strings.TrimSpace(d.Bytecode)
}
// ... and use assembler symbol for the operand if available/appropriate
if result.InstructionData != nil && (d.Operand == "" || d.Operand[0] != '?') {
if result.Defn.AddressingMode != instructions.Immediate {
switch result.Defn.Effect {
case instructions.Flow:
if result.Defn.AddressingMode == instructions.Relative {
// relative labels. to get the correct label we have to
// simulate what a successful branch instruction would do:
// -- we create a mock register with the instruction's
// address as the initial value
pc := registers.NewProgramCounter(result.Address)
// -- add the number of instruction bytes to get the PC as
// it would be at the end of the instruction
pc.Add(uint16(result.Defn.Bytes))
// -- because we're doing 16 bit arithmetic with an 8bit
// value, we need to make sure the sign bit has been
// propogated to the more-significant bits
if operand&0x0080 == 0x0080 {
operand |= 0xff00
}
// -- add the 2s-complement value to the mock program
// counter
pc.Add(operand)
// -- look up mock program counter value in symbol table
if v, ok := symtable.Locations.Symbols[pc.Address()]; ok {
d.Operand = v
}
} else {
if v, ok := symtable.Locations.Symbols[operand]; ok {
d.Operand = v
}
}
case instructions.Read:
if v, ok := symtable.Read.Symbols[operand]; ok {
d.Operand = v
}
case instructions.Write:
fallthrough
case instructions.RMW:
if v, ok := symtable.Write.Symbols[operand]; ok {
d.Operand = v
}
}
}
}
// decorate operand with addressing mode indicators
switch result.Defn.AddressingMode {
case instructions.Implied:
case instructions.Immediate:
d.Operand = fmt.Sprintf("#%s", d.Operand)
case instructions.Relative:
case instructions.Absolute:
case instructions.ZeroPage:
case instructions.Indirect:
d.Operand = fmt.Sprintf("(%s)", d.Operand)
case instructions.PreIndexedIndirect:
d.Operand = fmt.Sprintf("(%s,X)", d.Operand)
case instructions.PostIndexedIndirect:
d.Operand = fmt.Sprintf("(%s),Y", d.Operand)
case instructions.AbsoluteIndexedX:
d.Operand = fmt.Sprintf("%s,X", d.Operand)
case instructions.AbsoluteIndexedY:
d.Operand = fmt.Sprintf("%s,Y", d.Operand)
case instructions.IndexedZeroPageX:
d.Operand = fmt.Sprintf("%s,X", d.Operand)
case instructions.IndexedZeroPageY:
d.Operand = fmt.Sprintf("%s,Y", d.Operand)
default:
}
// cycles
d.Cycles = fmt.Sprintf("%d", result.ActualCycles)
// notes
if result.PageFault {
d.Notes = fmt.Sprintf("%s page-fault", d.Notes)
}
if result.Bug != "" {
d.Notes = fmt.Sprintf("%s * %s *", d.Notes, result.Bug)
}
return d, nil
}

View file

@ -0,0 +1,12 @@
// Package display facilitates the presentation of disassembled ROMs.
//
// The Instruction type stores the formatted parts of an individual
// disassembled instruction. Instruction should be instantiated with the
// Format command(). The Format() command takes an instance of execution.Result
// and annotates it for easy reading.
//
// The actual presentation of formatted results to the user is outside of the
// scope of this package but the Columns type is intended to help. The Update()
// function should be used to ensure that column widths are enough for all
// instances in a group of Instructions.
package display

View file

@ -1,11 +1,12 @@
// Package disassembly coordinates the disassembly of cartridge memory. For
// simple presentations of a cartridge the FromCartridge() function can be
// used. Many debuggers will probably find it more useful to disassemble from
// the memory of an already instantiated VCS.
// Package disassembly coordinates the disassembly Atari2600 (6507) cartridges.
//
// For quick disassemblies the FromCartridge() function can be used. Many
// debuggers will probably find it more useful however, to disassemble from the
// memory of an already instantiated VCS.
//
// disasm, _ := disassembly.FromMemory(cartMem, symbols.NewTable())
//
// The FromMemory() function requires a valid instance of a symbols.Table. In
// The FromMemory() function takes an instance of a symbols.Table, or nil. In
// the example above, we've used the result of NewTable(); which is fine but
// limits the potential of the disassembly package. For best results, the
// symbols.ReadSymbolsFile() function should be used (see symbols package for
@ -17,15 +18,15 @@
// information from cartridge memory. In a nutshell:
//
// Linear disassembly decodes every possible address in the cartridge. if the
// "execution" of the address succeeds it is stored in the linear table. Flow
// disassembly on the other hand decodes only those cartridge addresses that
// flow from the start adddress as the executable program unfolds.
// "execution" of the address succeeds the result is stored. Flow disassembly
// on the other hand decodes only those addresses that flow from the reset
// adddress as the program unfolds.
//
// In flow disassembly it is hoped that every branch and subroutine is
// considered. This is done by turning "flow control" off for the CPU and
// handling branches manually in the disassembly package. However, it maybe
// possible for correct CPU execution of the ROM to reach places not reachable
// by the flow. For example:
// In flow disassembly every branch and subroutine is considered. This is done
// by turning the CPU's "flow control" off and handling each and every the
// branch manually. Even with this method however, it is possible for a program
// to expect to be taken somewhere (when executed normally) not reachable. For
// example:
//
// - Addresses stuffed into the stack and RTS being called, without an explicit
// JSR.
@ -33,24 +34,27 @@
// - Branching or jumping to non-cartridge memory. (ie. RAM) and executing code
// there.
//
// The Analysis() function summarises any oddities like these that have been
// detected.
// The flow disassembly collates any possible oddities it encounters and the
// Analysis() function can be used to summarise them.
//
// Compared to flow disassembly, linear disassembly looks at every memory
// As already mentionied, linear disassembly looks at every possible memory
// location. The downside of this is that a lot of what is found will be
// nonsense (data segments never intended for execution, for instance). This
// make linear disassembly unsuitable for presentation of the entire ROM.
// Where linear disassembly *is* useful is a quick reference for an address
// that you know contains a valid instruction.
// make linear disassembly unsuitable for some applications. For example,
// presenting a disassembly of an entire cartridge.
//
// Where linear disassembly *is* useful is for referencing an address that you
// *know* contains a valid instruction - compare to flow disassembly where the
// address might not have been reached during the disassembly process.
//
// Note that linear cannot do anything about the posibility of executing code
// from area outside of cartridge space (ie. RAM).
//
// The flow/linear difference is invisible to the user of the disassembly
// package. Instead, the functions Get(), Dump() and Grep() are used. These
// functions use the most appropriate disassembly for the use case.
// All that said, the flow/linear difference is invisible to the user of the
// disassembly package. Instead, the functions Get(), Write() and Grep() are
// used. These functions use the most appropriate disassembly for the use case.
//
// Dump() --> flow
// Get() --> linear
// Grep() --> flow
// Write() --> flow
// Get() --> linear
// Grep() --> flow
package disassembly

45
disassembly/dump.go Normal file
View file

@ -0,0 +1,45 @@
package disassembly
import (
"fmt"
"gopher2600/disassembly/display"
"io"
)
// Write writes the entire disassembly to io.Writer
func (dsm *Disassembly) Write(output io.Writer, byteCode bool) {
for bank := 0; bank < len(dsm.flow); bank++ {
output.Write([]byte(fmt.Sprintf("--- bank %d ---\n", bank)))
for i := range dsm.flow[bank] {
if d := dsm.flow[bank][i]; d != nil {
dsm.WriteLine(output, byteCode, d)
}
}
}
}
// WriteLine writes a single Instruction to io.Writer
func (dsm *Disassembly) WriteLine(output io.Writer, byteCode bool, d *display.Instruction) {
if d.Location != "" {
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Location, d.Location)))
output.Write([]byte("\n"))
}
if byteCode {
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Bytecode, d.Bytecode)))
output.Write([]byte(" "))
}
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Address, d.Address)))
output.Write([]byte(" "))
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Mnemonic, d.Mnemonic)))
output.Write([]byte(" "))
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Operand, d.Operand)))
output.Write([]byte(" "))
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Cycles, d.Cycles)))
output.Write([]byte(" "))
output.Write([]byte(fmt.Sprintf(dsm.Columns.Fmt.Notes, d.Notes)))
output.Write([]byte("\n"))
}

View file

@ -1,39 +0,0 @@
package disassembly
import (
"gopher2600/hardware/cpu/instructions"
"gopher2600/hardware/cpu/result"
)
// Entry for every address in the cartridge
type Entry struct {
// if the type of entry is, or appears to be a, a valid instruction then
// instructionDefinition will be non-null
instructionDefinition *instructions.Definition
// to keep things simple, we're only keeping a string representation of the
// disassembly. we used to keep a instance of result.verbose but after
// some consideration, I don't like that - the result was obtained with an
// incomplete or misleading context, and so cannot be relied upon to give
// accurate information with regards to pagefaults etc. the simplest way
// around this is to record a string representation, containing only the
// information that's required.
//
// undefined if instructionDefinition == nil
instruction string
// the styling used to format the instruction member above
style result.Style
}
func (ent Entry) String() string {
if ent.instructionDefinition == nil {
return ""
}
return ent.instruction
}
// IsInstruction returns false if the entry does not represent an instruction
func (ent Entry) IsInstruction() bool {
return ent.instructionDefinition != nil
}

View file

@ -4,7 +4,6 @@ import (
"gopher2600/errors"
"gopher2600/hardware/cpu"
"gopher2600/hardware/cpu/instructions"
"gopher2600/hardware/cpu/result"
"gopher2600/hardware/memory/memorymap"
)
@ -32,23 +31,25 @@ func (dsm *Disassembly) flowDisassembly(mc *cpu.CPU) error {
}
}
// check validity of instruction result
err = mc.LastResult.IsValid()
if err != nil {
// fail on invalid results
if err := mc.LastResult.IsValid(); err != nil {
return err
}
bank := dsm.cart.GetBank(mc.LastResult.Address)
// if we've seen this before then finish the disassembly
if dsm.flow[bank][mc.LastResult.Address&disasmMask].IsInstruction() {
if dsm.flow[bank][mc.LastResult.Address&disasmMask] != nil {
return nil
}
dsm.flow[bank][mc.LastResult.Address&disasmMask] = Entry{
style: result.StyleDisasm,
instruction: mc.LastResult.GetString(dsm.Symtable, result.StyleDisasm),
instructionDefinition: mc.LastResult.Defn}
d, err := dsm.FormatResult(mc.LastResult)
if err != nil {
return err
}
dsm.Columns.Update(d)
dsm.flow[bank][mc.LastResult.Address&disasmMask] = d
// we've disabled flow-control in the cpu but we still need to pay
// attention to what's going on or we won't get to see all the areas of

View file

@ -1,16 +1,25 @@
package disassembly
import (
"bytes"
"fmt"
"io"
"strings"
)
// Grep searches the disassembly for the specified search string.
func (dsm *Disassembly) Grep(output io.Writer, search string, caseSensitive bool, contextLines uint) {
var s, m string
// GrepScope limits the scope of the search
type GrepScope int
ctx := make([]string, contextLines)
// List of available scopes
const (
GrepMnemonic GrepScope = iota
GrepOperand
GrepAll
)
// Grep searches the disassembly for the specified search string.
func (dsm *Disassembly) Grep(output io.Writer, scope GrepScope, search string, caseSensitive bool) {
var s, m string
if !caseSensitive {
search = strings.ToUpper(search)
@ -19,10 +28,25 @@ func (dsm *Disassembly) Grep(output io.Writer, search string, caseSensitive bool
for bank := 0; bank < len(dsm.flow); bank++ {
bankHeader := false
for a := 0; a < len(dsm.flow[bank]); a++ {
entry := dsm.flow[bank][a]
d := dsm.flow[bank][a]
if d != nil {
// line representation of Instruction. we'll print this
// in case of a match
line := &bytes.Buffer{}
dsm.WriteLine(line, false, d)
// limit scope of grep to the correct Instruction field
switch scope {
case GrepMnemonic:
s = d.Mnemonic
case GrepOperand:
s = d.Operand
case GrepAll:
s = line.String()
}
if entry.instructionDefinition != nil {
s = entry.instruction
if !caseSensitive {
m = strings.ToUpper(s)
} else {
@ -30,6 +54,9 @@ func (dsm *Disassembly) Grep(output io.Writer, search string, caseSensitive bool
}
if strings.Contains(m, search) {
// if we've not yet printed head for the current bank then
// print it now
if !bankHeader {
if bank > 0 {
output.Write([]byte("\n"))
@ -37,28 +64,10 @@ func (dsm *Disassembly) Grep(output io.Writer, search string, caseSensitive bool
output.Write([]byte(fmt.Sprintf("--- bank %d ---\n", bank)))
bankHeader = true
} else if contextLines > 0 {
output.Write([]byte("\n"))
}
// print context
for c := 0; c < len(ctx); c++ {
// only write actual content. note that there is more often
// than not, valid context. the main reason as far I can
// see, for empty context are mistakes in disassembly
if ctx[c] != "" {
output.Write([]byte(ctx[c]))
output.Write([]byte("\n"))
}
}
// print match
output.Write([]byte(s))
output.Write([]byte("\n"))
ctx = make([]string, contextLines)
} else if contextLines > 0 {
ctx = append(ctx[1:], s)
// we've matched so print entire line
output.Write(line.Bytes())
}
}
}

View file

@ -2,7 +2,6 @@ package disassembly
import (
"gopher2600/hardware/cpu"
"gopher2600/hardware/cpu/result"
"gopher2600/hardware/memory/memorymap"
)
@ -18,14 +17,20 @@ func (dsm *Disassembly) linearDisassembly(mc *cpu.CPU) error {
// deliberately ignoring errors
_ = mc.ExecuteInstruction(nil)
// check validity of instruction result and add if it "executed"
// correctly
if mc.LastResult.IsValid() == nil {
dsm.linear[bank][address&disasmMask] = Entry{
style: result.StyleBrief,
instruction: mc.LastResult.GetString(dsm.Symtable, result.StyleBrief),
instructionDefinition: mc.LastResult.Defn}
// continue for loop on invalid results. we don't want to be as
// discerning as in flowDisassembly(). the nature of
// linearDisassembly() means that we're likely to try executing
// invalid instructions. best just to ignore such errors.
if mc.LastResult.IsValid() != nil {
continue // for loop
}
ent, err := dsm.FormatResult(mc.LastResult)
if err != nil {
return err
}
dsm.linear[bank][address&disasmMask] = ent
}
}

View file

@ -217,6 +217,7 @@ func disasm(md *modalflag.Modes) error {
md.NewMode()
cartFormat := md.AddString("cartformat", "AUTO", "force use of cartridge format")
bytecode := md.AddBool("bytecode", false, "include bytecode in disassembly")
p, err := md.Parse()
if p != modalflag.ParseContinue {
@ -235,12 +236,12 @@ func disasm(md *modalflag.Modes) error {
if err != nil {
// print what disassembly output we do have
if dsm != nil {
dsm.Dump(md.Output)
dsm.Write(md.Output, *bytecode)
}
return errors.New(errors.DisassemblyError, err)
}
dsm.Dump(md.Output)
dsm.Write(md.Output, *bytecode)
default:
return fmt.Errorf("too many arguments for %s mode", md)
}

View file

@ -3,9 +3,9 @@ package cpu
import (
"fmt"
"gopher2600/errors"
"gopher2600/hardware/cpu/execution"
"gopher2600/hardware/cpu/instructions"
"gopher2600/hardware/cpu/registers"
"gopher2600/hardware/cpu/result"
"gopher2600/hardware/memory/addresses"
"gopher2600/hardware/memory/bus"
"log"
@ -41,7 +41,7 @@ type CPU struct {
RdyFlg bool
// last result
LastResult result.Instruction
LastResult execution.Result
// silently ignore addressing errors unless StrictAddressing is true
StrictAddressing bool
@ -72,8 +72,6 @@ func NewCPU(mem bus.CPUBus) (*CPU, error) {
mc.acc16 = registers.NewProgramCounter(0)
// set Final flag in LastResult to true because logically we can say it is.
// this fixes what might be considered a bug when building the debugger
// prompt. this is the best way of fixing it.
mc.LastResult.Final = true
var err error
@ -355,12 +353,13 @@ func (mc *CPU) ExecuteInstruction(cycleCallback func() error) error {
}
// prepare new round of results
mc.LastResult.Address = mc.PC.Address()
mc.LastResult.Defn = nil
mc.LastResult.Final = false
mc.LastResult.Address = mc.PC.Address()
mc.LastResult.InstructionData = nil
mc.LastResult.ActualCycles = 0
mc.LastResult.PageFault = false
mc.LastResult.Bug = ""
mc.LastResult.Final = false
// register end cycle callback
defer func() {

View file

@ -0,0 +1,11 @@
// Package execution tracks the result of instruction execution on the CPU.
// The Result type stores detailed information about each instruction
// encountered during a program execution on the CPU. A Result can then be used
// to produce output for disassemblers and debuggers with the help of the
// disassembly package.
//
// The Result.IsValid() function can be used to check whether results are
// consistent with the instruction definition. The CPU pcakage doesn't call
// this function because it would introduce unwanted performance penalties, but
// it's probably okay to use in a debugging context.
package execution

View file

@ -1,24 +1,25 @@
package result
package execution
import (
"gopher2600/hardware/cpu/instructions"
)
// Instruction contains all the interesting information about a CPU
// instruction. Including the address it was read from, the data that was used
// during the execution of the instruction and a reference to the instruction
// definition.
// Result records the state/result of each instruction executed on the CPU.
// Including the address it was read from, a reference to the instruction
// definition, and other execution details.
//
// Other fields record inforamtion about the last execution of this specific
// instruction, whether it caused a page fault, the actual number of cycles it
// took and any information about known bugs that might have been triggered in
// the CPU.
// The Result type is updated every cycle during the execution of the emulated
// CPU. As the execution continues, more information is acquired and detail
// added to the Result.
//
// The Instruction type is update every cycle during execution in the emulated
// CPU. As more information is known about the instuction it is added. The
// final field indicates whether the last cycle has been executed and the
// instruction decoding is complete.
type Instruction struct {
// The Final field indicates whether the last cycle of the instruction has been
// executed. An instance of Result with a Final value of false can still be
// used but with the caveat that the information is incomplete. Note that a
// Defn of nil means the opcode hasn't even been decoded.
type Result struct {
// a reference to the instruction definition
Defn *instructions.Definition
Address uint16
// it would be lovely to have a note of which cartridge bank the address is
@ -26,17 +27,10 @@ type Instruction struct {
// if you need to know the cartridge bank then you need to get it somehow
// else.
// a reference to the instruction definition
Defn *instructions.Definition
// instruction data is the actual instruction data. so, for example, in the
// case of ranch instruction, instruction data is the offset value.
// case of branch instruction, instruction data is the offset value.
InstructionData interface{}
// whether this data has been finalised - some fields in this struct will
// be undefined if Final is false
Final bool
// the actual number of cycles taken by the instruction - usually the same
// as Defn.Cycles but in the case of PageFaults and branches, this value
// may be different
@ -47,4 +41,8 @@ type Instruction struct {
// whether a known buggy code path (in the emulated CPU) was triggered
Bug string
// whether this data has been finalised - some fields in this struct will
// be undefined if Final is false
Final bool
}

View file

@ -1,4 +1,4 @@
package result
package execution
import (
"fmt"
@ -7,11 +7,9 @@ import (
"reflect"
)
// IsValid checks whether the instance of StepResult contains consistent data.
//
// Intended to be used during development of the CPU pacakge, to make sure
// implementation hasn't gone off the rails.
func (result Instruction) IsValid() error {
// IsValid checks whether the instance of Result contains information
// consistent with the instruction definition.
func (result Result) IsValid() error {
if !result.Final {
return errors.New(errors.InvalidResult, "not checking an unfinalised InstructionResult")
}

View file

@ -1,14 +0,0 @@
// Package result handles storage and presentation of CPU instruction results.
// The main product of this package is the Instruction type.
//
// The Instruction type is used by the CPU to store detailed information about
// each instruction executed. This type can then be used to produce output for
// disassemblers and debuggers with the GetString() function. The Style type
// helps the user of the package to control how the string is constructed.
//
// The Instruction.IsValid() function can be used to check whether the results
// CPU execution, as stored in the Insruction type, is consistent with the
// instruction definition. For example, the PageFault field is set to true:
// does the instruction definition suggest that this instruction can even cause
// page faults under certain conditions.
package result

View file

@ -1,232 +0,0 @@
package result
import (
"fmt"
"gopher2600/hardware/cpu/instructions"
"gopher2600/hardware/cpu/registers"
"gopher2600/symbols"
"strings"
)
// GetString returns a human readable version of InstructionResult, addresses
// replaced with symbols if supplied symbols argument is not null.
func (result Instruction) GetString(symtable *symbols.Table, style Style) string {
if symtable == nil {
panic(fmt.Sprintf("Instruction.GetString() requires a non-nil instance of symbols.Table"))
}
// columns
var hex string
var label, programCounter string
var operator, operand string
var notes string
// include instruction address (and label) if this is the final result for
// this particular instruction
if result.Final && style.Has(StyleFlagAddress) {
programCounter = fmt.Sprintf("0x%04x", result.Address)
if style.Has(StyleFlagLocation) {
if v, ok := symtable.Locations.Symbols[result.Address]; ok {
label = v
}
}
}
// use question marks where instruction hasn't been decoded yet
if result.Defn == nil {
// nothing has been decoded yet
operator = "???"
} else {
// use mnemonic if specified in instruciton result
operator = result.Defn.Mnemonic
// parse instruction result data ...
var idx uint16
switch result.InstructionData.(type) {
case uint8:
idx = uint16(result.InstructionData.(uint8))
operand = fmt.Sprintf("$%02x", idx)
case uint16:
idx = uint16(result.InstructionData.(uint16))
operand = fmt.Sprintf("$%04x", idx)
case nil:
if result.Defn.Bytes == 2 {
operand = "??"
} else if result.Defn.Bytes == 3 {
operand = "????"
}
}
// (include byte code in output)
if result.Final && style.Has(StyleFlagByteCode) {
switch result.Defn.Bytes {
case 3:
hex = fmt.Sprintf("%02x", idx&0xff00>>8)
fallthrough
case 2:
hex = fmt.Sprintf("%02x %s", idx&0x00ff, hex)
fallthrough
case 1:
hex = fmt.Sprintf("%02x %s", result.Defn.OpCode, hex)
default:
hex = fmt.Sprintf("(%d bytes) %s", result.Defn.Bytes, hex)
}
}
// ... and use assembler symbol for the operand if available/appropriate
if style.Has(StyleFlagSymbols) && result.InstructionData != nil && (operand == "" || operand[0] != '?') {
if result.Defn.AddressingMode != instructions.Immediate {
switch result.Defn.Effect {
case instructions.Flow:
if result.Defn.AddressingMode == instructions.Relative {
// relative labels. to get the correct label we have to
// simulate what a successful branch instruction would do:
// -- we create a mock register with the instruction's
// address as the initial value
pc := registers.NewProgramCounter(result.Address)
// -- add the number of instruction bytes to get the PC as
// it would be at the end of the instruction
pc.Add(uint16(result.Defn.Bytes))
// -- because we're doing 16 bit arithmetic with an 8bit
// value, we need to make sure the sign bit has been
// propogated to the more-significant bits
if idx&0x0080 == 0x0080 {
idx |= 0xff00
}
// -- add the 2s-complement value to the mock program
// counter
pc.Add(idx)
// -- look up mock program counter value in symbol table
if v, ok := symtable.Locations.Symbols[pc.Address()]; ok {
operand = v
}
} else {
if v, ok := symtable.Locations.Symbols[idx]; ok {
operand = v
}
}
case instructions.Read:
if v, ok := symtable.Read.Symbols[idx]; ok {
operand = v
}
case instructions.Write:
fallthrough
case instructions.RMW:
if v, ok := symtable.Write.Symbols[idx]; ok {
operand = v
}
}
}
}
// decorate operand with addressing mode indicators
switch result.Defn.AddressingMode {
case instructions.Implied:
case instructions.Immediate:
operand = fmt.Sprintf("#%s", operand)
case instructions.Relative:
case instructions.Absolute:
case instructions.ZeroPage:
case instructions.Indirect:
operand = fmt.Sprintf("(%s)", operand)
case instructions.PreIndexedIndirect:
operand = fmt.Sprintf("(%s,X)", operand)
case instructions.PostIndexedIndirect:
operand = fmt.Sprintf("(%s),Y", operand)
case instructions.AbsoluteIndexedX:
operand = fmt.Sprintf("%s,X", operand)
case instructions.AbsoluteIndexedY:
operand = fmt.Sprintf("%s,Y", operand)
case instructions.IndexedZeroPageX:
operand = fmt.Sprintf("%s,X", operand)
case instructions.IndexedZeroPageY:
operand = fmt.Sprintf("%s,Y", operand)
default:
}
}
if style.Has(StyleFlagCycles) {
if result.Final {
// result is of a complete instruction - add number of cycles it
// actually took to execute
notes = fmt.Sprintf("[%d]", result.ActualCycles)
} else {
// result is an interim result - indicate with [v], which means
// video cycle
notes = "[v]"
}
}
// add annotation
if style.Has(StyleFlagNotes) {
// add annotation for page-faults and known CPU bugs - these can occur
// whether or not the result is not yet 'final'
if result.PageFault {
notes += " page-fault"
}
if result.Bug != "" {
notes += fmt.Sprintf(" * %s *", result.Bug)
}
}
// force column widths
if style.Has(StyleFlagColumns) {
if style.Has(StyleFlagByteCode) {
hex = columnise(hex, 8)
}
programCounter = columnise(programCounter, 6)
operator = columnise(operator, 3)
if symtable.MaxLocationWidth > 0 {
label = columnise(label, symtable.MaxLocationWidth)
} else {
label = columnise(label, 0)
}
if symtable.MaxSymbolWidth > 0 {
// +3 to MaxSymbolWidth so that additional notation (parenthesis,
// etc.) isn't cropped off.
operand = columnise(operand, symtable.MaxSymbolWidth+3)
} else {
operand = columnise(operand, 7)
}
}
// build final string
s := fmt.Sprintf("%s %s %s %s %s %s",
hex,
label,
programCounter,
operator,
operand,
notes)
if style.Has(StyleFlagCompact) {
return strings.Trim(s, " ")
}
return s
}
// columnise forces the string into the given width. used for outputting
// disassembly into columns
func columnise(s string, width int) string {
if width > len(s) {
t := make([]byte, width-len(s))
for i := 0; i < len(t); i++ {
t[i] = ' '
}
s = fmt.Sprintf("%s%s", s, t)
} else if width < len(s) {
s = s[:width]
}
return s
}

View file

@ -1,44 +0,0 @@
package result
// Style specifies the elements to include in strings constructed by
// Instruction.GetString(). Different styles can be ORed together to produce
// custom styles.
type Style int
// List of valid Style flags.
const (
// ByteCode flag causes the program data to be printed verbatim before
// the disassembly
StyleFlagByteCode Style = 0x01 << iota
// specifying StyleFlagSymbols or StyleFlagLocation has no effect if no
// symbols type instance is available
StyleFlagAddress
StyleFlagSymbols
StyleFlagLocation
// the number of cycles consumed by the instruction
StyleFlagCycles
// include any useful notes about the disassembly. for example, whether a
// page-fault occurred
StyleFlagNotes
// force output into columns of suitable width
StyleFlagColumns
// remove leading/trailing whitespace
StyleFlagCompact
)
// List of compound styles. For convenience.
const (
StyleExecution = StyleFlagAddress | StyleFlagSymbols | StyleFlagLocation | StyleFlagCycles | StyleFlagNotes | StyleFlagColumns
StyleDisasm = StyleFlagByteCode | StyleFlagAddress | StyleFlagSymbols | StyleFlagLocation | StyleFlagColumns
StyleBrief = StyleFlagSymbols | StyleFlagCompact
)
// Has tests to see if style has the supplied flag in its definition
func (style Style) Has(flag Style) bool {
return style&flag == flag
}