From 636895a7e416e869376d2678d26ef6ad38f42e33 Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Sun, 7 Jun 2020 18:23:39 +0200 Subject: [PATCH] Trace ProDOS MLI calls --- README.md | 3 + apple2.go | 23 +++- apple2Setup.go | 5 +- apple2main.go | 7 +- core6502/execute.go | 10 ++ traceProDOS.go | 274 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 314 insertions(+), 8 deletions(-) create mode 100644 traceProDOS.go diff --git a/README.md b/README.md index 4ea46c1..dee5c12 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Portable emulator of an Apple II+ or //e. Written in Go. - Fast disk mode to set max speed while using the disks. - Single file executable with embedded ROMs and DOS 3.3 - Pause (thanks a2geek) + - ProDOS MLI calls tracing ## Running the emulator @@ -175,6 +176,8 @@ Only valid on SDL mode dump to the console the CPU execution. Use F11 to toggle. -traceHD dump to the console the hd commands + -traceMLI + dump to the console the calls to ProDOS machine langunage interface calls to $BF00 -traceSS dump to the console the sofswitches calls -vidHDSlot int diff --git a/apple2.go b/apple2.go index 7545592..f71432a 100644 --- a/apple2.go +++ b/apple2.go @@ -28,6 +28,7 @@ type Apple2 struct { profile bool showSpeed bool paused bool + traceMLI *traceProDOS } const ( @@ -51,10 +52,9 @@ func (a *Apple2) Run() { // Run a 6502 step if !a.paused { a.cpu.ExecuteInstruction() + a.executionTrace() } else { time.Sleep(200 * time.Millisecond) - referenceTime = time.Now() - speedReferenceTime = referenceTime } // Execute meta commands @@ -62,10 +62,17 @@ func (a *Apple2) Run() { for commandsPending { select { case command := <-a.commandChannel: - if command == CommandKill { + switch command { + case CommandKill: return + case CommandPauseUnpauseEmulator: + a.paused = !a.paused + referenceTime = time.Now() + speedReferenceTime = referenceTime + default: + // Execute the other commands + a.executeCommand(command) } - a.executeCommand(command) default: commandsPending = false } @@ -178,8 +185,6 @@ func (a *Apple2) executeCommand(command int) { a.cpu.SetTrace(!a.cpu.GetTrace()) case CommandReset: a.cpu.Reset() - case CommandPauseUnpauseEmulator: - a.paused = !a.paused } } @@ -196,6 +201,12 @@ func (a *Apple2) releaseFastMode() { } } +func (a *Apple2) executionTrace() { + if a.traceMLI != nil { + a.traceMLI.inspect() + } +} + type persistent interface { save(io.Writer) error load(io.Reader) error diff --git a/apple2Setup.go b/apple2Setup.go index dddcf5a..a887e7e 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -44,10 +44,13 @@ func newApple2eEnhanced() *Apple2 { return &a } -func (a *Apple2) setup(isColor bool, clockMhz float64, fastMode bool) { +func (a *Apple2) setup(isColor bool, clockMhz float64, fastMode bool, traceMLI bool) { a.commandChannel = make(chan int, 100) a.isColor = isColor a.fastMode = fastMode + if traceMLI { + a.traceMLI = newTraceProDOS(a) + } if clockMhz <= 0 { // Full speed diff --git a/apple2main.go b/apple2main.go index 41d734a..5ecc0cb 100644 --- a/apple2main.go +++ b/apple2main.go @@ -109,6 +109,11 @@ func MainApple() *Apple2 { "profile", false, "generate profile trace to analyse with pprof") + traceMLI := flag.Bool( + "traceMLI", + false, + "dump to the console the calls to ProDOS machine langunage interface calls to $BF00") + flag.Parse() if *wozImage != "" { @@ -178,7 +183,7 @@ func MainApple() *Apple2 { panic("Model not supported") } - a.setup(!*mono, *cpuClock, *fastDisk) + a.setup(!*mono, *cpuClock, *fastDisk, *traceMLI) a.cpu.SetTrace(*traceCPU) a.io.setTrace(*traceSS) a.io.setPanicNotImplemented(*panicSS) diff --git a/core6502/execute.go b/core6502/execute.go index a677267..42065a5 100644 --- a/core6502/execute.go +++ b/core6502/execute.go @@ -103,6 +103,16 @@ func (s *State) GetTrace() bool { return s.trace } +// GetPCAndSP returns the current program counter and stack pointer. Used to trace MLI calls +func (s *State) GetPCAndSP() (uint16, uint8) { + return s.reg.getPC(), s.reg.getSP() +} + +// GetCarryAndAcc returns the value of te carry flag and the accumulator. Used to trace MLI calls +func (s *State) GetCarryAndAcc() (bool, uint8) { + return s.reg.getFlag(flagC), s.reg.getA() +} + // Save saves the CPU state (registers and cycle counter) func (s *State) Save(w io.Writer) error { err := binary.Write(w, binary.BigEndian, s.cycles) diff --git a/traceProDOS.go b/traceProDOS.go new file mode 100644 index 0000000..aade8a9 --- /dev/null +++ b/traceProDOS.go @@ -0,0 +1,274 @@ +package apple2 + +import "fmt" + +type traceProDOS struct { + a *Apple2 + callPending bool // We assume MLI is not reentrant + functionCode uint8 + paramsAdddress uint16 + returnAddress uint16 +} + +const ( + mliAddress uint16 = 0xbf00 + biAddress uint16 = 0xbe03 +) + +func newTraceProDOS(a *Apple2) *traceProDOS { + var t traceProDOS + t.a = a + return &t +} + +func (t *traceProDOS) inspect() { + pc, ps := t.a.cpu.GetPCAndSP() + if pc == mliAddress { + /* + MLI has been called (provided we are running proDOS and the proper page) + Calls to MLI must be: + JSR $BF00 + DFB function_code + DW addr_op_parms + */ + if t.callPending { + if t.functionCode == 0x65 { + // QUIT when successfull does not return + fmt.Printf("Ok \n") + } else { + fmt.Print("\n") + } + } + caller := uint16(t.a.mmu.Peek(0x100+uint16(ps+1))) + + uint16(t.a.mmu.Peek(0x100+uint16(ps+2)))<<8 - 2 + t.functionCode = t.a.mmu.Peek(caller + 3) + t.paramsAdddress = uint16(t.a.mmu.Peek(caller+4)) + uint16(t.a.mmu.Peek(caller+5))<<8 + t.returnAddress = caller + 6 + fmt.Printf("MLI call $%02x from $%04x", t.functionCode, caller) + switch t.functionCode { + case 0x40: + fmt.Printf(" ALLOC_INTERRUPT()") + case 0x41: + fmt.Printf(" DEALLOC_INTERRUPT()") + case 0x65: + fmt.Printf(" QUIT()") + case 0x80: + fmt.Printf(" READ_BLOCK(unit=%s, block=$%04x)", parseUnit(t.paramByte(1)), t.paramWord(4)) + case 0x81: + fmt.Printf(" WRITE_BLOCK(unit=%s, block=$%04x)", parseUnit(t.paramByte(1)), t.paramWord(4)) + case 0x82: + fmt.Printf(" GET_TIME()") + case 0xc0: + fmt.Printf(" CREATE(\"%s\")", t.paramString(1)) + case 0xc1: + fmt.Printf(" DESTROY(\"%s\")", t.paramString(1)) + case 0xc2: + fmt.Printf(" RENAME(old=\"%s\", new=\"%s\")", t.paramString(1), t.paramString(3)) + case 0xc3: + fmt.Printf(" GET_FILE_INFO(\"%s\")", t.paramString(1)) + case 0xc4: + fmt.Printf(" SET_FILE_INFO(\"%s\")", t.paramString(1)) + case 0xc5: + fmt.Printf(" ONLINE(unit=%s)", parseUnit(t.paramByte(1))) + case 0xc6: + fmt.Printf(" SET_PREFIX(\"%s\")", t.paramString(1)) + case 0xc7: + fmt.Printf(" GET_PREFIX()") + case 0xc8: + fmt.Printf(" OPEN(\"%s\")", t.paramString(1)) + case 0xc9: + fmt.Printf(" NEWLINE(ref=%v, mask=$%02x, char=$%02x)", t.paramByte(1), t.paramByte(2), t.paramByte(3)) + case 0xca: + fmt.Printf(" READ(ref=%v, len=%v)", t.paramByte(1), t.paramWord(4)) + case 0xcb: + fmt.Printf(" WRITE(ref=%v, len=%v)", t.paramByte(1), t.paramWord(4)) + case 0xcc: + fmt.Printf(" CLOSE(ref=%v)", t.paramByte(1)) + case 0xcd: + fmt.Printf(" FLUSH(ref=%v)", t.paramByte(1)) + case 0xce: + fmt.Printf(" SET_MARK(ref=%v, pos=%v)", t.paramByte(1), t.paramLen(2)) + case 0xcf: + fmt.Printf(" GET_MARK(ref=%v)", t.paramByte(1)) + case 0xd1: + fmt.Printf(" GET_EOF(ref=%v)", t.paramByte(1)) + case 0xd2: + fmt.Printf(" SET_BUF(ref=%v)", t.paramByte(1)) + case 0xd3: + fmt.Printf(" GET_BUF(ref=%v)", t.paramByte(1)) + } + fmt.Printf(" => ") + + t.callPending = true + } else if t.callPending && pc == t.returnAddress { + error, acc := t.a.cpu.GetCarryAndAcc() + if error { + fmt.Printf("error $%02x: %v\n", acc, getMliErrorText(acc)) + } else { + switch t.functionCode { + case 0x82: // Get Time + // Globals will be updated + date := uint16(t.a.mmu.Peek(0xbf90)) + uint16(t.a.mmu.Peek(0xbf91))<<8 + minute := t.a.mmu.Peek(0xbf92) + hour := t.a.mmu.Peek(0xbf93) + fmt.Printf("%04v-%02v-%02v %02v:%02v\n", + date>>9+1900, (date>>5)&0x1f, date&0x1f, // Review Y2K + hour, minute) + case 0xc5: // Online + dataAddress := t.paramWord(2) + for { + b := t.a.mmu.Peek(dataAddress) + dataAddress++ + if b == 0 { + fmt.Printf("\n") + break + } + unit := parseUnit(b) + size := b & 0xf + if size != 0 { + // No error + name := "" + for i := uint8(0); i < size; i++ { + name += string(t.a.mmu.Peek(dataAddress+uint16(i)) & 0x7f) + } + fmt.Printf("%s: \"%s\" ", unit, name) + } else { + err := t.a.mmu.Peek(dataAddress) + fmt.Printf("%s: error $%02x ", unit, err) + } + if t.paramByte(1) != 0 { + fmt.Printf("\n") + break // Only one entry requested + } + dataAddress += 15 + } + case 0xc7: // Get prefix + fmt.Printf("\"%v\"\n", t.paramString(1)) + case 0xc8: // Open file + fmt.Printf("ref: %v\n", t.paramByte(5)) + case 0xca: // Read + fmt.Printf("%v bytes read \n", t.paramByte(6)) + case 0xcb: // Write + fmt.Printf("%v bytes written \n", t.paramByte(6)) + case 0xcf: // File position + fmt.Printf("%v\n", t.paramLen(2)) + case 0xd1: // File size + fmt.Printf("%v bytes\n", t.paramLen(2)) + default: + fmt.Printf("Ok\n") + } + } + t.callPending = false + } else if pc == biAddress { + s := "" + for i := uint16(1); i < 256; i++ { + ch := t.a.mmu.Peek(0x200 + i) + if ch == 0 || ch == 0x8d { + break + } + s += string(ch) + } + fmt.Printf("Prodos BI exec: \"%s\".\n", s) + } +} + +func (t *traceProDOS) paramByte(pos uint16) uint8 { + return t.a.mmu.Peek(t.paramsAdddress + pos) +} + +func (t *traceProDOS) paramWord(pos uint16) uint16 { + // Two bytes + return uint16(t.a.mmu.Peek(t.paramsAdddress+pos)) + uint16(t.a.mmu.Peek(t.paramsAdddress+pos+1))<<8 +} + +func (t *traceProDOS) paramLen(pos uint16) uint32 { + // Three bytes + return uint32(t.paramWord(pos)) + uint32(t.paramByte(pos+2))<<16 +} + +func (t *traceProDOS) paramString(pos uint16) string { + address := t.paramWord(pos) + size := t.a.mmu.Peek(address) + s := "" + for i := uint8(0); i < size; i++ { + s += string(t.a.mmu.Peek(address+1+uint16(i)) & 0x7f) + } + return s +} + +func parseUnit(unit uint8) string { + if unit == 0 { + return "All" + } + drive := unit >> 7 + slot := (unit >> 4) & 7 + return fmt.Sprintf("S%v,D%v", slot, drive+1) +} + +func getMliErrorText(code uint8) string { + // From https://prodos8.com/docs/techref/quick-reference-card/ + switch code { + case 0x00: + return "No error" + case 0x01: + return "Bad system call number" + case 0x04: + return "Bad system call parameter count" + case 0x25: + return "Interrupt table full" + case 0x27: + return "I/O error" + case 0x28: + return "No device connected" + case 0x2B: + return "Disk write protected" + case 0x2E: + return "Disk switched" + case 0x40: + return "Invalid pathname" + case 0x42: + return "Maximum number of files open" + case 0x43: + return "Invalid reference number" + case 0x44: + return "Directory not found" + case 0x45: + return "Volume not found" + case 0x46: + return "File not found" + case 0x47: + return "Duplicate filename" + case 0x48: + return "Volume full" + case 0x49: + return "Volume directory full" + case 0x4A: + return "Incompatible file format, also a ProDOS directory" + case 0x4B: + return "Unsupported storage_type" + case 0x4C: + return "End of file encountered" + case 0x4D: + return "Position out of range" + case 0x4E: + return "File access error, also file locked" + case 0x50: + return "File is open" + case 0x51: + return "Directory structure damaged" + case 0x52: + return "Not a ProDOS volume" + case 0x53: + return "Invalid system call parameter" + case 0x55: + return "Volume Control Block table full" + case 0x56: + return "Bad buffer address" + case 0x57: + return "Duplicate volume" + case 0x5A: + return "File structure damaged" + default: + return "Unknown error" + } +}