2021-03-12 06:03:05 +00:00
|
|
|
import { debug, toHex } from './util';
|
|
|
|
import { byte, word } from './types';
|
|
|
|
|
|
|
|
import CPU6502, { DebugInfo, flags, sizes } from './cpu6502';
|
|
|
|
|
|
|
|
export interface DebuggerContainer {
|
|
|
|
run: () => void;
|
|
|
|
stop: () => void;
|
2022-06-20 02:42:34 +00:00
|
|
|
isRunning: () => boolean;
|
2021-03-12 06:03:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type symbols = { [key: number]: string };
|
2022-05-10 13:52:06 +00:00
|
|
|
type breakpointFn = (info: DebugInfo) => boolean;
|
2021-03-12 06:03:05 +00:00
|
|
|
|
|
|
|
const alwaysBreak = (_info: DebugInfo) => { return true; };
|
|
|
|
|
2021-10-13 16:15:29 +00:00
|
|
|
export const dumpStatusRegister = (sr: byte) =>
|
|
|
|
[
|
|
|
|
(sr & flags.N) ? 'N' : '-',
|
|
|
|
(sr & flags.V) ? 'V' : '-',
|
|
|
|
(sr & flags.X) ? 'X' : '-',
|
|
|
|
(sr & flags.B) ? 'B' : '-',
|
|
|
|
(sr & flags.D) ? 'D' : '-',
|
|
|
|
(sr & flags.I) ? 'I' : '-',
|
|
|
|
(sr & flags.Z) ? 'Z' : '-',
|
|
|
|
(sr & flags.C) ? 'C' : '-',
|
|
|
|
].join('');
|
|
|
|
|
2021-03-12 06:03:05 +00:00
|
|
|
export default class Debugger {
|
|
|
|
private verbose = false;
|
|
|
|
private maxTrace = 256;
|
|
|
|
private trace: DebugInfo[] = [];
|
|
|
|
private breakpoints: Map<word, breakpointFn> = new Map();
|
|
|
|
private symbols: symbols = {};
|
|
|
|
|
2022-07-14 03:34:50 +00:00
|
|
|
constructor(private cpu: CPU6502, private container: DebuggerContainer) {}
|
2021-03-12 06:03:05 +00:00
|
|
|
|
|
|
|
stepCycles(cycles: number) {
|
|
|
|
this.cpu.stepCyclesDebug(this.verbose ? 1 : cycles, () => {
|
|
|
|
const info = this.cpu.getDebugInfo();
|
|
|
|
|
|
|
|
if (this.breakpoints.get(info.pc)?.(info)) {
|
|
|
|
debug('breakpoint', this.printDebugInfo(info));
|
|
|
|
this.container.stop();
|
2021-03-14 00:08:24 +00:00
|
|
|
return true;
|
2021-03-12 06:03:05 +00:00
|
|
|
}
|
|
|
|
if (this.verbose) {
|
|
|
|
debug(this.printDebugInfo(info));
|
|
|
|
} else {
|
2021-03-14 00:08:24 +00:00
|
|
|
this.updateTrace(info);
|
2021-03-12 06:03:05 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-03-14 00:08:24 +00:00
|
|
|
break = () => {
|
|
|
|
this.container.stop();
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-14 00:08:24 +00:00
|
|
|
|
|
|
|
step = () => {
|
|
|
|
this.cpu.step(() => {
|
|
|
|
const info = this.cpu.getDebugInfo();
|
|
|
|
debug(this.printDebugInfo(info));
|
|
|
|
this.updateTrace(info);
|
|
|
|
});
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-14 00:08:24 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
continue = () => {
|
2021-03-12 06:03:05 +00:00
|
|
|
this.container.run();
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2022-07-04 20:10:47 +00:00
|
|
|
/**
|
|
|
|
* Restart at a given memory address.
|
|
|
|
*
|
|
|
|
* @param address Address to start execution
|
|
|
|
*/
|
|
|
|
|
|
|
|
runAt = (address: word) => {
|
|
|
|
this.cpu.reset();
|
|
|
|
this.cpu.setPC(address);
|
|
|
|
};
|
|
|
|
|
2022-06-20 02:42:34 +00:00
|
|
|
isRunning = () =>
|
|
|
|
this.container.isRunning();
|
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
setVerbose = (verbose: boolean) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
this.verbose = verbose;
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
setMaxTrace = (maxTrace: number) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
this.maxTrace = maxTrace;
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2022-06-20 02:42:34 +00:00
|
|
|
getTrace = (count?: number) => {
|
|
|
|
return this.trace.slice(count ? -count : undefined).map(this.printDebugInfo).join('\n');
|
|
|
|
};
|
|
|
|
|
|
|
|
printTrace = (count?: number) => {
|
|
|
|
debug(this.getTrace(count));
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2022-06-20 02:42:34 +00:00
|
|
|
getStack = (size?: number) => {
|
|
|
|
const { sp } = this.cpu.getDebugInfo();
|
|
|
|
const stack = [];
|
|
|
|
|
|
|
|
let max = 255;
|
|
|
|
let min = 0;
|
|
|
|
if (size) {
|
|
|
|
if ((sp - 3) >= (255 - size)) {
|
|
|
|
min = Math.max(255 - size + 1, 0);
|
|
|
|
} else {
|
|
|
|
max = Math.min(sp + size - 4, 255);
|
|
|
|
min = Math.max(sp - 3, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let addr = max; addr >= min; addr--) {
|
|
|
|
const isSP = addr === sp ? '*' : ' ';
|
|
|
|
const addrStr = `$${toHex(0x0100 + addr)}`;
|
|
|
|
const valStr = toHex(this.cpu.read(0x01, addr));
|
|
|
|
if (!size || ((sp + size > addr) && (addr > sp - size))) {
|
|
|
|
stack.push(`${isSP} ${addrStr} ${valStr}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return stack.join('\n');
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
setBreakpoint = (addr: word, exp?: breakpointFn) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
this.breakpoints.set(addr, exp || alwaysBreak);
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
clearBreakpoint = (addr: word) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
this.breakpoints.delete(addr);
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
listBreakpoints = () => {
|
2021-03-12 06:03:05 +00:00
|
|
|
for(const [addr, fn] of this.breakpoints.entries()) {
|
|
|
|
debug(toHex(addr, 4), fn);
|
|
|
|
}
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
addSymbols = (symbols: symbols) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
this.symbols = { ...this.symbols, ...symbols };
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
printDebugInfo = (info: DebugInfo) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
const { pc, cmd } = info;
|
|
|
|
const symbol = this.padWithSymbol(pc);
|
|
|
|
|
|
|
|
return [
|
|
|
|
toHex(pc, 4),
|
|
|
|
'- ', symbol,
|
|
|
|
this.dumpRegisters(info),
|
|
|
|
' ',
|
|
|
|
this.dumpRawOp(cmd),
|
|
|
|
' ',
|
|
|
|
this.dumpOp(pc, cmd)
|
|
|
|
].join('');
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
dumpPC = (pc: word) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
const b = this.cpu.read(pc);
|
|
|
|
const op = this.cpu.getOpInfo(b);
|
|
|
|
const size = sizes[op.mode];
|
|
|
|
let result = toHex(pc, 4) + '- ';
|
|
|
|
|
|
|
|
result += this.padWithSymbol(pc);
|
|
|
|
|
2022-05-31 15:38:40 +00:00
|
|
|
const cmd = new Array<number>(size);
|
2021-03-12 06:03:05 +00:00
|
|
|
for (let idx = 0, jdx = pc; idx < size; idx++, jdx++) {
|
|
|
|
cmd[idx] = this.cpu.read(jdx);
|
|
|
|
}
|
|
|
|
|
|
|
|
result += this.dumpRawOp(cmd) + ' ' + this.dumpOp(pc, cmd);
|
|
|
|
|
|
|
|
return result;
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
dumpRegisters = (debugInfo?: DebugInfo) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
if (debugInfo === undefined) {
|
|
|
|
debugInfo = this.cpu.getDebugInfo();
|
|
|
|
}
|
2021-03-14 00:08:24 +00:00
|
|
|
const { ar, xr, yr, sr, sp } = debugInfo;
|
2021-03-12 06:03:05 +00:00
|
|
|
return [
|
2021-03-14 00:54:09 +00:00
|
|
|
'A=' + toHex(ar),
|
2021-03-12 06:03:05 +00:00
|
|
|
' X=' + toHex(xr),
|
|
|
|
' Y=' + toHex(yr),
|
|
|
|
' P=' + toHex(sr),
|
|
|
|
' S=' + toHex(sp),
|
|
|
|
' ',
|
2021-10-13 16:15:29 +00:00
|
|
|
dumpStatusRegister(sr),
|
2021-03-12 06:03:05 +00:00
|
|
|
].join('');
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
dumpPage = (start: byte, end?: byte) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
let result = '';
|
|
|
|
if (end === undefined) {
|
|
|
|
end = start;
|
|
|
|
}
|
|
|
|
for (let page = start; page <= end; page++) {
|
|
|
|
for (let idx = 0; idx < 16; idx++) {
|
|
|
|
result += toHex(page) + toHex(idx << 4) + ': ';
|
|
|
|
for (let jdx = 0; jdx < 16; jdx++) {
|
|
|
|
const b = this.cpu.read(page, idx * 16 + jdx);
|
|
|
|
result += toHex(b) + ' ';
|
|
|
|
}
|
|
|
|
result += ' ';
|
|
|
|
for (let jdx = 0; jdx < 16; jdx++) {
|
|
|
|
const b = this.cpu.read(page, idx * 16 + jdx) & 0x7f;
|
|
|
|
if (b >= 0x20 && b < 0x7f) {
|
|
|
|
result += String.fromCharCode(b);
|
|
|
|
} else {
|
|
|
|
result += '.';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result += '\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2022-07-04 20:10:47 +00:00
|
|
|
/**
|
|
|
|
* Reads a range of memory. Will wrap at memory limit.
|
|
|
|
*
|
|
|
|
* @param address Starting address to read memory
|
|
|
|
* @param length Length of memory to read.
|
|
|
|
* @returns Byte array containing memory
|
|
|
|
*/
|
|
|
|
getMemory(address: word, length: word) {
|
|
|
|
const bytes = new Uint8Array(length);
|
|
|
|
for (let idx = 0; idx < length; idx++) {
|
|
|
|
address &= 0xffff;
|
|
|
|
bytes[idx] = this.cpu.read(address++);
|
|
|
|
}
|
|
|
|
return bytes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes a range of memory. Will wrap at memory limit.
|
|
|
|
*
|
|
|
|
* @param address Starting address to write memory
|
|
|
|
* @param bytes Data to write
|
|
|
|
*/
|
|
|
|
setMemory(address: word, bytes: Uint8Array) {
|
|
|
|
for (const byte of bytes) {
|
|
|
|
address &= 0xffff;
|
|
|
|
this.cpu.write(address++, byte);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-13 20:36:19 +00:00
|
|
|
list = (pc: word) => {
|
2021-03-12 06:03:05 +00:00
|
|
|
const results = [];
|
|
|
|
for (let idx = 0; idx < 20; idx++) {
|
|
|
|
const b = this.cpu.read(pc);
|
|
|
|
const op = this.cpu.getOpInfo(b);
|
|
|
|
results.push(this.dumpPC(pc));
|
|
|
|
pc += sizes[op.mode];
|
|
|
|
}
|
|
|
|
return results;
|
2021-11-29 00:20:25 +00:00
|
|
|
};
|
2021-03-12 06:03:05 +00:00
|
|
|
|
2021-03-14 00:08:24 +00:00
|
|
|
private updateTrace(info: DebugInfo) {
|
|
|
|
this.trace.push(info);
|
|
|
|
if (this.trace.length > this.maxTrace) {
|
|
|
|
this.trace.shift();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 06:03:05 +00:00
|
|
|
private padWithSymbol(pc: word): string {
|
|
|
|
const padding = ' ';
|
|
|
|
const symbol = this.symbols[pc];
|
|
|
|
|
|
|
|
let result: string = padding;
|
|
|
|
if (symbol) {
|
|
|
|
result = `${symbol}${padding.substring(symbol.length)}`;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
private dumpRawOp(parts: byte[]) {
|
|
|
|
const result = new Array(4);
|
|
|
|
for (let idx = 0; idx < 4; idx++) {
|
|
|
|
if (idx < parts.length) {
|
|
|
|
result[idx] = toHex(parts[idx]);
|
|
|
|
} else {
|
|
|
|
result[idx] = ' ';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result.join(' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
private dumpOp(pc: word, parts: byte[]) {
|
|
|
|
const op = this.cpu.getOpInfo(parts[0]);
|
|
|
|
const lsb = parts[1];
|
|
|
|
const msb = parts[2];
|
|
|
|
const addr = (msb << 8) | lsb;
|
|
|
|
let val;
|
|
|
|
let off;
|
|
|
|
const toHexOrSymbol = (v: word, n?: number) => (
|
|
|
|
this.symbols[v] || ('$' + toHex(v, n))
|
|
|
|
);
|
|
|
|
|
|
|
|
let result = op.name + ' ';
|
|
|
|
switch (op.mode) {
|
|
|
|
case 'implied':
|
|
|
|
break;
|
|
|
|
case 'immediate':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `#${toHexOrSymbol(lsb)}`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'absolute':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(addr, 4)}`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'zeroPage':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(lsb)}`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'relative':
|
|
|
|
{
|
|
|
|
off = lsb;
|
|
|
|
if (off > 127) {
|
|
|
|
off -= 256;
|
|
|
|
}
|
2021-03-14 00:08:24 +00:00
|
|
|
pc += off + 2;
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(pc, 4)} (${off})`;
|
2021-03-12 06:03:05 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'absoluteX':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(addr, 4)},X`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'absoluteY':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(addr, 4)},Y`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'zeroPageX':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(lsb)},X`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'zeroPageY':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(lsb)},Y`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'absoluteIndirect':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `(${toHexOrSymbol(addr, 4)})`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'zeroPageXIndirect':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `(${toHexOrSymbol(lsb)},X)`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'zeroPageIndirectY':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `(${toHexOrSymbol(lsb)},),Y`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'accumulator':
|
|
|
|
result += 'A';
|
|
|
|
break;
|
|
|
|
case 'zeroPageIndirect':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `(${toHexOrSymbol(lsb)})`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'absoluteXIndirect':
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `(${toHexOrSymbol(addr, 4)},X)`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
case 'zeroPage_relative':
|
|
|
|
val = lsb;
|
|
|
|
off = msb;
|
|
|
|
if (off > 127) {
|
|
|
|
off -= 256;
|
|
|
|
}
|
|
|
|
pc += off + 2;
|
2022-05-31 15:38:40 +00:00
|
|
|
result += `${toHexOrSymbol(val)},${toHexOrSymbol(pc, 4)} (${off})`;
|
2021-03-12 06:03:05 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|