mirror of
https://github.com/JorjBauer/aiie.git
synced 2026-10-22 17:02:22 +00:00
506 lines
12 KiB
C++
506 lines
12 KiB
C++
#include "debugger.h"
|
|
|
|
#ifdef AIIE_DEBUGGER
|
|
|
|
#include <string.h>
|
|
#include "globals.h"
|
|
#include "cpu.h"
|
|
#include "applemmu.h"
|
|
#include "applekeyboard.h"
|
|
#include "disassembler.h"
|
|
|
|
Debugger g_debugger;
|
|
|
|
static Disassembler dbgDis;
|
|
|
|
static const DebugBank cpuBank = { DBG_BANK_CPU, 0, 1 };
|
|
|
|
static AppleMMU *theMMU()
|
|
{
|
|
return g_vm ? (AppleMMU *)g_vm->getMMU() : NULL;
|
|
}
|
|
|
|
Debugger::Debugger()
|
|
{
|
|
runState = DBG_RUNNING;
|
|
isArmed = false;
|
|
haltAvailable = false;
|
|
haltRequested = false;
|
|
resuming = false;
|
|
stepMode = STEP_NONE;
|
|
stepReason = DBG_HALT_STEP;
|
|
runToAddr = 0;
|
|
stepOutSP = 0;
|
|
lastOpcode = 0;
|
|
numBreakpoints = 0;
|
|
numWatchpoints = 0;
|
|
watchCount = 0;
|
|
traceOn = false;
|
|
traceHead = 0;
|
|
traceFill = 0;
|
|
memset(&lastHalt, 0, sizeof(lastHalt));
|
|
memset(&requested, 0, sizeof(requested));
|
|
}
|
|
|
|
// ---- run control ----------------------------------------------------------
|
|
|
|
void Debugger::rearm()
|
|
{
|
|
isArmed = (runState == DBG_PAUSED) || haltRequested || resuming ||
|
|
(numBreakpoints > 0) || (stepMode != STEP_NONE) || traceOn;
|
|
}
|
|
|
|
void Debugger::halt(DebugHaltReason reason, uint16_t addr)
|
|
{
|
|
runState = DBG_PAUSED;
|
|
lastHalt.reason = reason;
|
|
lastHalt.addr = addr;
|
|
lastHalt.pc = g_cpu->pc;
|
|
lastHalt.isWrite = false;
|
|
lastHalt.value = 0;
|
|
haltAvailable = true;
|
|
haltRequested = false;
|
|
stepMode = STEP_NONE;
|
|
rearm();
|
|
}
|
|
|
|
void Debugger::resume(StepMode mode)
|
|
{
|
|
runState = DBG_RUNNING;
|
|
resuming = true;
|
|
stepMode = mode;
|
|
haltRequested = false;
|
|
rearm();
|
|
}
|
|
|
|
void Debugger::pause()
|
|
{
|
|
if (runState == DBG_PAUSED) return;
|
|
requested.reason = DBG_HALT_PAUSE;
|
|
requested.addr = g_cpu->pc;
|
|
requested.isWrite = false;
|
|
requested.value = 0;
|
|
haltRequested = true;
|
|
rearm();
|
|
}
|
|
|
|
void Debugger::cont()
|
|
{
|
|
if (runState == DBG_RUNNING && stepMode == STEP_NONE && !haltRequested) return;
|
|
resume(STEP_NONE);
|
|
}
|
|
|
|
void Debugger::stepIn()
|
|
{
|
|
stepReason = DBG_HALT_STEP;
|
|
resume(STEP_IN);
|
|
}
|
|
|
|
void Debugger::stepOver()
|
|
{
|
|
if (peek(g_cpu->pc, cpuBank) == 0x20) { // JSR: run until it comes back
|
|
runToAddr = g_cpu->pc + 3;
|
|
stepReason = DBG_HALT_STEP;
|
|
resume(STEP_RUNTO);
|
|
} else {
|
|
stepIn();
|
|
}
|
|
}
|
|
|
|
void Debugger::stepOut()
|
|
{
|
|
stepOutSP = g_cpu->sp;
|
|
lastOpcode = 0;
|
|
stepReason = DBG_HALT_STEP;
|
|
resume(STEP_OUT);
|
|
}
|
|
|
|
void Debugger::runTo(uint16_t addr)
|
|
{
|
|
runToAddr = addr;
|
|
stepReason = DBG_HALT_RUNTO;
|
|
resume(STEP_RUNTO);
|
|
}
|
|
|
|
bool Debugger::takeHalt(DebugHalt *out)
|
|
{
|
|
if (!haltAvailable) return false;
|
|
if (out) *out = lastHalt;
|
|
haltAvailable = false;
|
|
return true;
|
|
}
|
|
|
|
// Called by Cpu::step() before every instruction while armed. The PC is the
|
|
// instruction about to run.
|
|
bool Debugger::onInstruction()
|
|
{
|
|
if (runState == DBG_PAUSED) return true;
|
|
|
|
uint16_t pc = g_cpu->pc;
|
|
|
|
if (resuming) {
|
|
// The instruction at PC runs no matter what: this is how cont() gets
|
|
// past the breakpoint it stopped on.
|
|
resuming = false;
|
|
rearm();
|
|
} else {
|
|
if (haltRequested) {
|
|
DebugHaltReason reason = requested.reason;
|
|
halt(reason, requested.addr);
|
|
lastHalt.isWrite = requested.isWrite;
|
|
lastHalt.value = requested.value;
|
|
return true;
|
|
}
|
|
|
|
int bi = breakpointIndex(pc);
|
|
if (bi >= 0 && breakpoints[bi].enabled) {
|
|
halt(DBG_HALT_BREAKPOINT, pc);
|
|
return true;
|
|
}
|
|
|
|
switch (stepMode) {
|
|
case STEP_NONE:
|
|
break;
|
|
case STEP_IN:
|
|
halt(DBG_HALT_STEP, pc);
|
|
return true;
|
|
case STEP_OUT:
|
|
// An RTS or RTI just executed and the stack is above where we
|
|
// started: the subroutine we were in has returned.
|
|
if ((lastOpcode == 0x60 || lastOpcode == 0x40) && g_cpu->sp > stepOutSP) {
|
|
halt(DBG_HALT_STEP, pc);
|
|
return true;
|
|
}
|
|
break;
|
|
case STEP_RUNTO:
|
|
if (pc == runToAddr) {
|
|
halt(stepReason, pc);
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (stepMode == STEP_OUT) lastOpcode = peek(pc, cpuBank);
|
|
|
|
if (traceOn) {
|
|
trace[traceHead] = pc;
|
|
traceHead = (traceHead + 1) % DBG_TRACE_DEPTH;
|
|
if (traceFill < DBG_TRACE_DEPTH) traceFill++;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Called by AppleMMU::read and write when any watchpoint is set.
|
|
void Debugger::onAccess(uint16_t addr, uint8_t v, bool isWrite)
|
|
{
|
|
if (runState == DBG_PAUSED || haltRequested) return;
|
|
for (int i = 0; i < numWatchpoints; i++) {
|
|
const DebugWatchpoint &w = watchpoints[i];
|
|
if (!w.enabled) continue;
|
|
if (addr < w.from || addr > w.to) continue;
|
|
if (isWrite ? !w.onWrite : !w.onRead) continue;
|
|
requested.reason = DBG_HALT_WATCHPOINT;
|
|
requested.addr = addr;
|
|
requested.isWrite = isWrite;
|
|
requested.value = v;
|
|
haltRequested = true;
|
|
rearm();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// ---- breakpoints ----------------------------------------------------------
|
|
|
|
int Debugger::breakpointIndex(uint16_t addr)
|
|
{
|
|
for (int i = 0; i < numBreakpoints; i++) {
|
|
if (breakpoints[i].addr == addr) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int Debugger::addBreakpoint(uint16_t addr)
|
|
{
|
|
if (numBreakpoints >= DBG_MAX_BREAKPOINTS) return -1;
|
|
if (breakpointIndex(addr) >= 0) return -1;
|
|
breakpoints[numBreakpoints].addr = addr;
|
|
breakpoints[numBreakpoints].enabled = true;
|
|
numBreakpoints++;
|
|
rearm();
|
|
return numBreakpoints - 1;
|
|
}
|
|
|
|
bool Debugger::removeBreakpoint(uint16_t addr)
|
|
{
|
|
int i = breakpointIndex(addr);
|
|
if (i < 0) return false;
|
|
for (; i < numBreakpoints - 1; i++) breakpoints[i] = breakpoints[i+1];
|
|
numBreakpoints--;
|
|
rearm();
|
|
return true;
|
|
}
|
|
|
|
void Debugger::enableBreakpoint(int index, bool on)
|
|
{
|
|
if (index < 0 || index >= numBreakpoints) return;
|
|
breakpoints[index].enabled = on;
|
|
}
|
|
|
|
void Debugger::clearBreakpoints()
|
|
{
|
|
numBreakpoints = 0;
|
|
rearm();
|
|
}
|
|
|
|
int Debugger::breakpointCount()
|
|
{
|
|
return numBreakpoints;
|
|
}
|
|
|
|
const DebugBreakpoint *Debugger::breakpoint(int index)
|
|
{
|
|
if (index < 0 || index >= numBreakpoints) return NULL;
|
|
return &breakpoints[index];
|
|
}
|
|
|
|
// ---- watchpoints ----------------------------------------------------------
|
|
|
|
int Debugger::addWatchpoint(uint16_t from, uint16_t to, bool onRead, bool onWrite)
|
|
{
|
|
if (numWatchpoints >= DBG_MAX_WATCHPOINTS) return -1;
|
|
if (to < from) { uint16_t t = from; from = to; to = t; }
|
|
DebugWatchpoint &w = watchpoints[numWatchpoints];
|
|
w.from = from;
|
|
w.to = to;
|
|
w.onRead = onRead;
|
|
w.onWrite = onWrite;
|
|
w.enabled = true;
|
|
numWatchpoints++;
|
|
watchCount = numWatchpoints;
|
|
return numWatchpoints - 1;
|
|
}
|
|
|
|
bool Debugger::removeWatchpoint(int index)
|
|
{
|
|
if (index < 0 || index >= numWatchpoints) return false;
|
|
for (int i = index; i < numWatchpoints - 1; i++) watchpoints[i] = watchpoints[i+1];
|
|
numWatchpoints--;
|
|
watchCount = numWatchpoints;
|
|
return true;
|
|
}
|
|
|
|
void Debugger::enableWatchpoint(int index, bool on)
|
|
{
|
|
if (index < 0 || index >= numWatchpoints) return;
|
|
watchpoints[index].enabled = on;
|
|
}
|
|
|
|
void Debugger::clearWatchpoints()
|
|
{
|
|
numWatchpoints = 0;
|
|
watchCount = 0;
|
|
}
|
|
|
|
int Debugger::watchpointCount()
|
|
{
|
|
return numWatchpoints;
|
|
}
|
|
|
|
const DebugWatchpoint *Debugger::watchpoint(int index)
|
|
{
|
|
if (index < 0 || index >= numWatchpoints) return NULL;
|
|
return &watchpoints[index];
|
|
}
|
|
|
|
// ---- memory ---------------------------------------------------------------
|
|
|
|
uint8_t Debugger::peek(uint16_t addr, const DebugBank &bank)
|
|
{
|
|
AppleMMU *mmu = theMMU();
|
|
return mmu ? mmu->peek(addr, bank) : 0;
|
|
}
|
|
|
|
void Debugger::poke(uint16_t addr, uint8_t v, const DebugBank &bank)
|
|
{
|
|
AppleMMU *mmu = theMMU();
|
|
if (mmu) mmu->poke(addr, v, bank);
|
|
}
|
|
|
|
void Debugger::peekRange(uint16_t addr, uint8_t *out, uint16_t len, const DebugBank &bank)
|
|
{
|
|
for (uint16_t i = 0; i < len; i++) out[i] = peek((uint16_t)(addr + i), bank);
|
|
}
|
|
|
|
void Debugger::pokeRange(uint16_t addr, const uint8_t *in, uint16_t len, const DebugBank &bank)
|
|
{
|
|
for (uint16_t i = 0; i < len; i++) poke((uint16_t)(addr + i), in[i], bank);
|
|
}
|
|
|
|
// ---- registers ------------------------------------------------------------
|
|
|
|
void Debugger::setPC(uint16_t pc)
|
|
{
|
|
g_cpu->pc = pc;
|
|
// A step in progress was about a different instruction stream.
|
|
stepMode = STEP_NONE;
|
|
rearm();
|
|
}
|
|
|
|
void Debugger::setRegisters(uint8_t a, uint8_t x, uint8_t y, uint8_t sp, uint8_t flags)
|
|
{
|
|
g_cpu->a = a;
|
|
g_cpu->x = x;
|
|
g_cpu->y = y;
|
|
g_cpu->sp = sp;
|
|
g_cpu->flags = flags;
|
|
}
|
|
|
|
// ---- disassembly ----------------------------------------------------------
|
|
|
|
uint8_t Debugger::disassemble(uint16_t addr, const DebugBank &bank, char *out, uint16_t outSize)
|
|
{
|
|
uint8_t bytes[3];
|
|
peekRange(addr, bytes, 3, bank);
|
|
return dbgDis.instructionToOperands(addr, bytes, out, outSize);
|
|
}
|
|
|
|
// ---- trace ----------------------------------------------------------------
|
|
|
|
void Debugger::setTrace(bool on)
|
|
{
|
|
if (on && !traceOn) {
|
|
traceHead = 0;
|
|
traceFill = 0;
|
|
}
|
|
traceOn = on;
|
|
rearm();
|
|
}
|
|
|
|
uint32_t Debugger::traceCount()
|
|
{
|
|
return traceFill;
|
|
}
|
|
|
|
uint16_t Debugger::traceEntry(uint32_t back)
|
|
{
|
|
if (back >= traceFill) return 0;
|
|
// traceHead is the slot after the newest entry.
|
|
uint32_t idx = (traceHead + DBG_TRACE_DEPTH - 1 - back) % DBG_TRACE_DEPTH;
|
|
return trace[idx];
|
|
}
|
|
|
|
// ---- machine state --------------------------------------------------------
|
|
|
|
uint16_t Debugger::softSwitches()
|
|
{
|
|
AppleMMU *mmu = theMMU();
|
|
return mmu ? mmu->videoSwitches() : 0;
|
|
}
|
|
|
|
// Fold an Apple screen code (normal / inverse / flashing / lowercase) down
|
|
// to a printable ASCII byte.
|
|
static char foldScreenChar(uint8_t c)
|
|
{
|
|
c &= 0x7F;
|
|
if (c < 0x20) c += 0x40;
|
|
return (char)c;
|
|
}
|
|
|
|
// The first byte of visible text row 'row' (0..23). Apple II text rows are
|
|
// interleaved: base + 0x80*(row&7) + 0x28*(row>>3).
|
|
static uint16_t textRowBase(uint16_t base, uint8_t row)
|
|
{
|
|
return base + 0x80 * (row & 7) + 0x28 * (row >> 3);
|
|
}
|
|
|
|
uint8_t Debugger::textScreen(char out[24][80], uint16_t base)
|
|
{
|
|
static const DebugBank mainBank = { DBG_BANK_MAIN, 0, 1 };
|
|
static const DebugBank auxBank = { DBG_BANK_AUX, 0, 1 };
|
|
|
|
memset(out, 0, 24 * 80);
|
|
|
|
uint16_t sw = softSwitches();
|
|
if (base == 0 && (sw & S_80COL)) {
|
|
// 80-column text always lives on page 1 ($400): aux RAM holds the even
|
|
// (left) column of each cell and main RAM the odd (right) column.
|
|
for (uint8_t row = 0; row < 24; row++) {
|
|
uint16_t rowBase = textRowBase(0x400, row);
|
|
for (uint8_t i = 0; i < 40; i++) {
|
|
out[row][i*2] = foldScreenChar(peek(rowBase + i, auxBank));
|
|
out[row][i*2 + 1] = foldScreenChar(peek(rowBase + i, mainBank));
|
|
}
|
|
}
|
|
return 80;
|
|
}
|
|
|
|
if (base == 0) {
|
|
// 80STORE on pins the scanner to page 1; PAGE2 is then only a
|
|
// main/aux steering bit for the CPU's $0400-$07FF accesses.
|
|
base = ((sw & S_PAGE2) && !(sw & S_80STORE)) ? 0x800 : 0x400;
|
|
}
|
|
for (uint8_t row = 0; row < 24; row++) {
|
|
uint16_t rowBase = textRowBase(base, row);
|
|
for (uint8_t col = 0; col < 40; col++) {
|
|
out[row][col] = foldScreenChar(peek(rowBase + col, mainBank));
|
|
}
|
|
}
|
|
return 40;
|
|
}
|
|
|
|
// ---- keyboard injection ---------------------------------------------------
|
|
|
|
static uint8_t hexNibble(char x)
|
|
{
|
|
if (x >= '0' && x <= '9') return x - '0';
|
|
if (x >= 'a' && x <= 'f') return x - 'a' + 10;
|
|
if (x >= 'A' && x <= 'F') return x - 'A' + 10;
|
|
return 0;
|
|
}
|
|
|
|
uint16_t Debugger::injectKeys(const char *s)
|
|
{
|
|
if (!g_vm) return 0;
|
|
AppleKeyboard *kbd = (AppleKeyboard *)g_vm->getKeyboard();
|
|
uint16_t queued = 0;
|
|
const char *p = s;
|
|
while (*p) {
|
|
uint8_t c;
|
|
if (*p == '\\' && *(p+1)) {
|
|
p++;
|
|
switch (*p) {
|
|
case 'r': case 'n': c = 0x0D; break;
|
|
case 't': c = 0x09; break;
|
|
case 'e': c = 0x1B; break;
|
|
case '0': c = 0x00; break;
|
|
case '\\': c = 0x5C; break;
|
|
case 'x':
|
|
if (*(p+1) && *(p+2)) {
|
|
c = (hexNibble(*(p+1)) << 4) | hexNibble(*(p+2));
|
|
p += 2;
|
|
} else {
|
|
c = 'x';
|
|
}
|
|
break;
|
|
default: c = (uint8_t)*p; break;
|
|
}
|
|
p++;
|
|
} else {
|
|
c = (uint8_t)*p++;
|
|
}
|
|
if (!kbd->injectByte(c)) break;
|
|
queued++;
|
|
}
|
|
return queued;
|
|
}
|
|
|
|
uint16_t Debugger::injectQueueDepth()
|
|
{
|
|
if (!g_vm) return 0;
|
|
return ((AppleKeyboard *)g_vm->getKeyboard())->injectQueueDepth();
|
|
}
|
|
|
|
#endif // AIIE_DEBUGGER
|