Add advanced debugging tools
Protocol layer: - Add watchpoint support (setWatchpoint, toggleCheckpoint, listWatchpoints) - Add snapshot methods (saveSnapshot, loadSnapshot) - Add autostart support - Update command codes to match VICE binary monitor protocol - Refactor BreakpointInfo to CheckpointInfo for unified handling New tools: - toggleBreakpoint: Enable/disable breakpoints without deleting - setWatchpoint: Memory read/write watchpoints - listWatchpoints: Show active watchpoints - runTo: Run until specific address (temporary breakpoint) - disassemble: 6502 disassembler with KERNAL/BASIC labels - saveSnapshot/loadSnapshot: Machine state persistence - loadProgram: Autostart PRG/D64/T64 files Utilities: - Add full 6502 disassembler with all addressing modes - Include KERNAL/BASIC entry point labels 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
56ea0984bb
commit
cebe6f4a14
5 changed files with 989 additions and 17 deletions
382
src/index.ts
382
src/index.ts
|
|
@ -11,6 +11,8 @@ import {
|
|||
getVideoAddresses,
|
||||
getGraphicsMode,
|
||||
isSpriteVisible,
|
||||
disassemble,
|
||||
getLabelForAddress,
|
||||
} from "./utils/index.js";
|
||||
|
||||
const server = new McpServer({
|
||||
|
|
@ -644,8 +646,8 @@ Related tools: setBreakpoint, deleteBreakpoint`,
|
|||
breakpoints: breakpoints.map((bp) => ({
|
||||
id: bp.id,
|
||||
address: {
|
||||
value: bp.address,
|
||||
hex: `$${bp.address.toString(16).padStart(4, "0")}`,
|
||||
value: bp.startAddress,
|
||||
hex: `$${bp.startAddress.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
enabled: bp.enabled,
|
||||
temporary: bp.temporary,
|
||||
|
|
@ -655,6 +657,382 @@ Related tools: setBreakpoint, deleteBreakpoint`,
|
|||
}
|
||||
);
|
||||
|
||||
// Tool: enableBreakpoint / disableBreakpoint - Toggle breakpoint state
|
||||
server.registerTool(
|
||||
"toggleBreakpoint",
|
||||
{
|
||||
description: `Enable or disable a breakpoint without deleting it.
|
||||
|
||||
Use this to temporarily disable breakpoints while keeping their configuration.
|
||||
|
||||
Related tools: setBreakpoint, deleteBreakpoint, listBreakpoints`,
|
||||
inputSchema: z.object({
|
||||
breakpointId: z.number().describe("Breakpoint ID from setBreakpoint"),
|
||||
enabled: z.boolean().describe("True to enable, false to disable"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
await client.toggleCheckpoint(args.breakpointId, args.enabled);
|
||||
return formatResponse({
|
||||
success: true,
|
||||
breakpointId: args.breakpointId,
|
||||
enabled: args.enabled,
|
||||
message: `Breakpoint ${args.breakpointId} ${args.enabled ? "enabled" : "disabled"}`,
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: setWatchpoint - Set memory watchpoint
|
||||
server.registerTool(
|
||||
"setWatchpoint",
|
||||
{
|
||||
description: `Set a memory watchpoint to stop when memory is read or written.
|
||||
|
||||
Watchpoints are powerful for debugging:
|
||||
- "Why is this value changing?" → Use store watchpoint
|
||||
- "What's reading this address?" → Use load watchpoint
|
||||
- "Track all access to this region" → Use both
|
||||
|
||||
Range can be single address or address range (e.g., $D800-$DBFF for color RAM).
|
||||
|
||||
Related tools: deleteBreakpoint, listWatchpoints, continue`,
|
||||
inputSchema: z.object({
|
||||
startAddress: z.number().min(0).max(0xffff).describe("Start address of watched range (0x0000-0xFFFF)"),
|
||||
endAddress: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(0xffff)
|
||||
.optional()
|
||||
.describe("End address of watched range (default: same as start for single address)"),
|
||||
type: z.enum(["load", "store", "both"]).describe("Watch type: 'load' (read), 'store' (write), or 'both'"),
|
||||
enabled: z.boolean().optional().describe("Whether watchpoint is active (default: true)"),
|
||||
temporary: z.boolean().optional().describe("Auto-delete after hit (default: false)"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const endAddr = args.endAddress ?? args.startAddress;
|
||||
const id = await client.setWatchpoint(args.startAddress, endAddr, args.type, {
|
||||
enabled: args.enabled ?? true,
|
||||
temporary: args.temporary ?? false,
|
||||
});
|
||||
|
||||
const isSingleAddress = args.startAddress === endAddr;
|
||||
|
||||
return formatResponse({
|
||||
success: true,
|
||||
watchpointId: id,
|
||||
startAddress: {
|
||||
value: args.startAddress,
|
||||
hex: `$${args.startAddress.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
endAddress: {
|
||||
value: endAddr,
|
||||
hex: `$${endAddr.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
type: args.type,
|
||||
enabled: args.enabled ?? true,
|
||||
temporary: args.temporary ?? false,
|
||||
message: isSingleAddress
|
||||
? `Watchpoint ${id} set at $${args.startAddress.toString(16).padStart(4, "0")} (${args.type})`
|
||||
: `Watchpoint ${id} set for $${args.startAddress.toString(16).padStart(4, "0")}-$${endAddr.toString(16).padStart(4, "0")} (${args.type})`,
|
||||
hint: "Use continue() to run until watchpoint is triggered",
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: listWatchpoints - List all watchpoints
|
||||
server.registerTool(
|
||||
"listWatchpoints",
|
||||
{
|
||||
description: `List all active memory watchpoints.
|
||||
|
||||
Shows watchpoint IDs, address ranges, type (load/store), and status.
|
||||
|
||||
Related tools: setWatchpoint, deleteBreakpoint, listBreakpoints`,
|
||||
},
|
||||
async () => {
|
||||
const watchpoints = client.listWatchpoints();
|
||||
|
||||
if (watchpoints.length === 0) {
|
||||
return formatResponse({
|
||||
count: 0,
|
||||
watchpoints: [],
|
||||
hint: "No watchpoints set. Use setWatchpoint() to add one.",
|
||||
});
|
||||
}
|
||||
|
||||
return formatResponse({
|
||||
count: watchpoints.length,
|
||||
watchpoints: watchpoints.map((wp) => ({
|
||||
id: wp.id,
|
||||
startAddress: {
|
||||
value: wp.startAddress,
|
||||
hex: `$${wp.startAddress.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
endAddress: {
|
||||
value: wp.endAddress,
|
||||
hex: `$${wp.endAddress.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
type: wp.type,
|
||||
enabled: wp.enabled,
|
||||
temporary: wp.temporary,
|
||||
})),
|
||||
hint: `${watchpoints.length} watchpoint(s) active. Use deleteBreakpoint(id) to remove.`,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: runTo - Run until specific address
|
||||
server.registerTool(
|
||||
"runTo",
|
||||
{
|
||||
description: `Run execution until a specific address is reached.
|
||||
|
||||
Sets a temporary breakpoint at the target address and continues execution.
|
||||
The breakpoint is automatically deleted when hit.
|
||||
|
||||
Use for:
|
||||
- "Run until this function" → runTo(functionAddress)
|
||||
- "Skip to the end of this loop" → runTo(addressAfterLoop)
|
||||
|
||||
Related tools: continue, step, setBreakpoint`,
|
||||
inputSchema: z.object({
|
||||
address: z.number().min(0).max(0xffff).describe("Address to run to (0x0000-0xFFFF)"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
// Set temporary breakpoint
|
||||
const bpId = await client.setBreakpoint(args.address, { temporary: true });
|
||||
|
||||
// Continue execution
|
||||
await client.continue();
|
||||
|
||||
return formatResponse({
|
||||
running: true,
|
||||
targetAddress: {
|
||||
value: args.address,
|
||||
hex: `$${args.address.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
temporaryBreakpointId: bpId,
|
||||
message: `Running to $${args.address.toString(16).padStart(4, "0")}`,
|
||||
hint: "Execution will stop when target address is reached. Use status() to check state.",
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: disassemble - Disassemble memory
|
||||
server.registerTool(
|
||||
"disassemble",
|
||||
{
|
||||
description: `Disassemble 6502 machine code at a memory address.
|
||||
|
||||
Returns human-readable assembly instructions with:
|
||||
- Address and raw bytes
|
||||
- Mnemonic and operand
|
||||
- Branch target addresses (for branch instructions)
|
||||
- Known KERNAL/BASIC labels
|
||||
|
||||
Options:
|
||||
- address: Start address (default: current PC)
|
||||
- count: Number of instructions (default: 10)
|
||||
|
||||
Related tools: readMemory, getRegisters, step`,
|
||||
inputSchema: z.object({
|
||||
address: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(0xffff)
|
||||
.optional()
|
||||
.describe("Start address (default: current PC)"),
|
||||
count: z.number().min(1).max(100).optional().describe("Number of instructions to disassemble (default: 10)"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
// Get PC if no address specified
|
||||
let startAddress = args.address;
|
||||
if (startAddress === undefined) {
|
||||
const regResponse = await client.getRegisters();
|
||||
const count = regResponse.body.readUInt16LE(0);
|
||||
let offset = 2;
|
||||
for (let i = 0; i < count && offset < regResponse.body.length; i++) {
|
||||
const id = regResponse.body[offset];
|
||||
const size = regResponse.body[offset + 1];
|
||||
offset += 2;
|
||||
if (id === 3 && size === 2) {
|
||||
startAddress = regResponse.body.readUInt16LE(offset);
|
||||
break;
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
startAddress = startAddress ?? 0;
|
||||
}
|
||||
|
||||
const instructionCount = args.count || 10;
|
||||
|
||||
// Read enough bytes (max 3 bytes per instruction)
|
||||
const bytesToRead = Math.min(instructionCount * 3, 0x10000 - startAddress);
|
||||
const endAddress = Math.min(startAddress + bytesToRead - 1, 0xffff);
|
||||
const memData = await client.readMemory(startAddress, endAddress);
|
||||
|
||||
// Disassemble
|
||||
const instructions = disassemble(memData, startAddress, instructionCount);
|
||||
|
||||
// Add labels for known addresses
|
||||
const instructionsWithLabels = instructions.map((instr) => {
|
||||
const label = getLabelForAddress(instr.address);
|
||||
const targetLabel = instr.branchTarget ? getLabelForAddress(instr.branchTarget) : undefined;
|
||||
return {
|
||||
...instr,
|
||||
label,
|
||||
targetLabel,
|
||||
};
|
||||
});
|
||||
|
||||
// Format for output
|
||||
const lines = instructionsWithLabels.map((instr) => {
|
||||
let line = `${instr.addressHex}: ${instr.bytesHex.padEnd(8)} ${instr.fullInstruction}`;
|
||||
if (instr.label) line += ` ; ${instr.label}`;
|
||||
if (instr.targetLabel) line += ` ; -> ${instr.targetLabel}`;
|
||||
return line;
|
||||
});
|
||||
|
||||
return formatResponse({
|
||||
startAddress: {
|
||||
value: startAddress,
|
||||
hex: `$${startAddress.toString(16).padStart(4, "0")}`,
|
||||
},
|
||||
instructionCount: instructions.length,
|
||||
instructions: instructionsWithLabels,
|
||||
listing: lines.join("\n"),
|
||||
hint:
|
||||
instructions.length > 0 && instructions[0].mnemonic === "BRK"
|
||||
? "First instruction is BRK - this might be uninitialized memory or data"
|
||||
: `Disassembled ${instructions.length} instruction(s) from $${startAddress.toString(16).padStart(4, "0")}`,
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: saveSnapshot - Save machine state
|
||||
server.registerTool(
|
||||
"saveSnapshot",
|
||||
{
|
||||
description: `Save the complete machine state to a file.
|
||||
|
||||
Creates a VICE snapshot file containing:
|
||||
- All memory (RAM, I/O states)
|
||||
- CPU registers
|
||||
- VIC-II, SID, CIA states
|
||||
- Disk drive state (if attached)
|
||||
|
||||
Use to:
|
||||
- Save state before risky debugging
|
||||
- Create restore points
|
||||
- Share exact machine state
|
||||
|
||||
Related tools: loadSnapshot`,
|
||||
inputSchema: z.object({
|
||||
filename: z.string().describe("Filename for the snapshot (e.g., 'debug-state.vsf')"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
await client.saveSnapshot(args.filename);
|
||||
return formatResponse({
|
||||
success: true,
|
||||
filename: args.filename,
|
||||
message: `Snapshot saved to ${args.filename}`,
|
||||
hint: "Use loadSnapshot() to restore this state later",
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: loadSnapshot - Load machine state
|
||||
server.registerTool(
|
||||
"loadSnapshot",
|
||||
{
|
||||
description: `Load a previously saved machine state from a file.
|
||||
|
||||
Restores complete machine state including memory, registers, and peripheral states.
|
||||
|
||||
Warning: This completely replaces the current state!
|
||||
|
||||
Related tools: saveSnapshot`,
|
||||
inputSchema: z.object({
|
||||
filename: z.string().describe("Filename of the snapshot to load"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
await client.loadSnapshot(args.filename);
|
||||
return formatResponse({
|
||||
success: true,
|
||||
filename: args.filename,
|
||||
message: `Snapshot loaded from ${args.filename}`,
|
||||
hint: "Machine state restored. Use getRegisters() to verify state.",
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: loadProgram - Autostart a program
|
||||
server.registerTool(
|
||||
"loadProgram",
|
||||
{
|
||||
description: `Load and optionally run a program file.
|
||||
|
||||
Supports PRG, D64, T64, and other C64 file formats.
|
||||
For disk images, can specify which file to run.
|
||||
|
||||
Options:
|
||||
- run: If true (default), starts execution after loading
|
||||
- fileIndex: For disk images, which file to load (0 = first)
|
||||
|
||||
Related tools: reset, status, setBreakpoint`,
|
||||
inputSchema: z.object({
|
||||
filename: z.string().describe("Path to the program file (PRG, D64, T64, etc.)"),
|
||||
run: z.boolean().optional().describe("Run after loading (default: true)"),
|
||||
fileIndex: z.number().optional().describe("File index in disk image (default: 0)"),
|
||||
}),
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
await client.autostart(args.filename, args.fileIndex ?? 0, args.run ?? true);
|
||||
return formatResponse({
|
||||
success: true,
|
||||
filename: args.filename,
|
||||
run: args.run ?? true,
|
||||
message: `Loading ${args.filename}${args.run !== false ? " and running" : ""}`,
|
||||
hint: args.run !== false
|
||||
? "Program is loading. Set breakpoints before it reaches your code of interest."
|
||||
: "Program loaded but not started. Use continue() to run.",
|
||||
});
|
||||
} catch (error) {
|
||||
return formatError(error as ViceError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// SEMANTIC LAYER TOOLS - Interpreted output for autonomous debugging
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
ResponseType,
|
||||
ErrorCode,
|
||||
MemorySpace,
|
||||
CheckpointOp,
|
||||
ViceResponse,
|
||||
ConnectionState,
|
||||
} from "./types.js";
|
||||
|
|
@ -18,13 +19,20 @@ export interface ViceError {
|
|||
suggestion?: string;
|
||||
}
|
||||
|
||||
export interface BreakpointInfo {
|
||||
export type CheckpointType = "exec" | "load" | "store";
|
||||
|
||||
export interface CheckpointInfo {
|
||||
id: number;
|
||||
address: number;
|
||||
startAddress: number;
|
||||
endAddress: number;
|
||||
enabled: boolean;
|
||||
temporary: boolean;
|
||||
type: CheckpointType;
|
||||
}
|
||||
|
||||
// Keep for backwards compatibility
|
||||
export type BreakpointInfo = CheckpointInfo;
|
||||
|
||||
export class ViceClient {
|
||||
private socket: Socket | null = null;
|
||||
private requestId = 0;
|
||||
|
|
@ -42,8 +50,8 @@ export class ViceClient {
|
|||
port: 0,
|
||||
running: true,
|
||||
};
|
||||
// Track breakpoints locally (VICE doesn't have a reliable list command in all versions)
|
||||
private breakpoints = new Map<number, BreakpointInfo>();
|
||||
// Track checkpoints locally (VICE doesn't have a reliable list command in all versions)
|
||||
private checkpoints = new Map<number, CheckpointInfo>();
|
||||
|
||||
// Event handlers for async events (breakpoints, etc.)
|
||||
public onStopped?: (response: ViceResponse) => void;
|
||||
|
|
@ -376,6 +384,34 @@ export class ViceClient {
|
|||
return this.sendCommand(Command.RegistersGet, body);
|
||||
}
|
||||
|
||||
async setRegisters(
|
||||
registers: Array<{ id: number; value: number; size: 1 | 2 }>,
|
||||
memspace: MemorySpace = MemorySpace.MainCPU
|
||||
): Promise<void> {
|
||||
// Build body: memspace(1) + count(2) + [id(1) + size(1) + value(1|2)]...
|
||||
let bodySize = 3; // memspace + count
|
||||
for (const reg of registers) {
|
||||
bodySize += 2 + reg.size; // id + size + value
|
||||
}
|
||||
const body = Buffer.alloc(bodySize);
|
||||
body[0] = memspace;
|
||||
body.writeUInt16LE(registers.length, 1);
|
||||
|
||||
let offset = 3;
|
||||
for (const reg of registers) {
|
||||
body[offset] = reg.id;
|
||||
body[offset + 1] = reg.size;
|
||||
if (reg.size === 1) {
|
||||
body[offset + 2] = reg.value & 0xff;
|
||||
} else {
|
||||
body.writeUInt16LE(reg.value, offset + 2);
|
||||
}
|
||||
offset += 2 + reg.size;
|
||||
}
|
||||
|
||||
await this.sendCommand(Command.RegistersSet, body);
|
||||
}
|
||||
|
||||
async continue(): Promise<void> {
|
||||
await this.sendCommand(Command.Continue);
|
||||
this.state.running = true;
|
||||
|
|
@ -390,6 +426,14 @@ export class ViceClient {
|
|||
return response;
|
||||
}
|
||||
|
||||
async advanceInstructions(count: number, stepOver = false): Promise<ViceResponse> {
|
||||
const body = Buffer.alloc(3);
|
||||
body[0] = stepOver ? 1 : 0;
|
||||
body.writeUInt16LE(count, 1);
|
||||
const response = await this.sendCommand(Command.AdvanceInstructions, body);
|
||||
return response;
|
||||
}
|
||||
|
||||
async reset(hard = false): Promise<void> {
|
||||
const body = Buffer.alloc(1);
|
||||
body[0] = hard ? 1 : 0;
|
||||
|
|
@ -420,34 +464,159 @@ export class ViceClient {
|
|||
body.writeUInt16LE(address, 2);
|
||||
body[4] = stop ? 1 : 0;
|
||||
body[5] = enabled ? 1 : 0;
|
||||
body[6] = 0x01; // Exec
|
||||
body[6] = CheckpointOp.Exec;
|
||||
body[7] = temporary ? 1 : 0;
|
||||
|
||||
const response = await this.sendCommand(Command.CheckpointSet, body);
|
||||
const id = response.body.readUInt32LE(0);
|
||||
|
||||
// Track locally
|
||||
this.breakpoints.set(id, {
|
||||
this.checkpoints.set(id, {
|
||||
id,
|
||||
address,
|
||||
startAddress: address,
|
||||
endAddress: address,
|
||||
enabled,
|
||||
temporary,
|
||||
type: "exec",
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async setWatchpoint(
|
||||
startAddress: number,
|
||||
endAddress: number,
|
||||
type: "load" | "store" | "both",
|
||||
options: {
|
||||
enabled?: boolean;
|
||||
stop?: boolean;
|
||||
temporary?: boolean;
|
||||
} = {}
|
||||
): Promise<number> {
|
||||
const { enabled = true, stop = true, temporary = false } = options;
|
||||
|
||||
if (startAddress < 0 || startAddress > 0xffff) {
|
||||
throw this.makeError(
|
||||
"INVALID_ADDRESS",
|
||||
`Start address 0x${startAddress.toString(16)} is outside C64 memory range`,
|
||||
"C64 addresses are 16-bit (0x0000-0xFFFF)"
|
||||
);
|
||||
}
|
||||
if (endAddress < 0 || endAddress > 0xffff) {
|
||||
throw this.makeError(
|
||||
"INVALID_ADDRESS",
|
||||
`End address 0x${endAddress.toString(16)} is outside C64 memory range`,
|
||||
"C64 addresses are 16-bit (0x0000-0xFFFF)"
|
||||
);
|
||||
}
|
||||
if (startAddress > endAddress) {
|
||||
throw this.makeError(
|
||||
"INVALID_RANGE",
|
||||
`Start address (0x${startAddress.toString(16)}) is greater than end address (0x${endAddress.toString(16)})`,
|
||||
"Swap the addresses or check your range"
|
||||
);
|
||||
}
|
||||
|
||||
// Determine operation type
|
||||
let op: number;
|
||||
let checkpointType: CheckpointType;
|
||||
if (type === "load") {
|
||||
op = CheckpointOp.Load;
|
||||
checkpointType = "load";
|
||||
} else if (type === "store") {
|
||||
op = CheckpointOp.Store;
|
||||
checkpointType = "store";
|
||||
} else {
|
||||
op = CheckpointOp.Load | CheckpointOp.Store;
|
||||
checkpointType = "load"; // Will track as load for simplicity
|
||||
}
|
||||
|
||||
// Build request: start(2) + end(2) + stop(1) + enabled(1) + op(1) + temp(1)
|
||||
const body = Buffer.alloc(8);
|
||||
body.writeUInt16LE(startAddress, 0);
|
||||
body.writeUInt16LE(endAddress, 2);
|
||||
body[4] = stop ? 1 : 0;
|
||||
body[5] = enabled ? 1 : 0;
|
||||
body[6] = op;
|
||||
body[7] = temporary ? 1 : 0;
|
||||
|
||||
const response = await this.sendCommand(Command.CheckpointSet, body);
|
||||
const id = response.body.readUInt32LE(0);
|
||||
|
||||
// Track locally
|
||||
this.checkpoints.set(id, {
|
||||
id,
|
||||
startAddress,
|
||||
endAddress,
|
||||
enabled,
|
||||
temporary,
|
||||
type: checkpointType,
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async toggleCheckpoint(checkpointId: number, enabled: boolean): Promise<void> {
|
||||
const body = Buffer.alloc(5);
|
||||
body.writeUInt32LE(checkpointId, 0);
|
||||
body[4] = enabled ? 1 : 0;
|
||||
await this.sendCommand(Command.CheckpointToggle, body);
|
||||
|
||||
// Update local tracking
|
||||
const cp = this.checkpoints.get(checkpointId);
|
||||
if (cp) {
|
||||
cp.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBreakpoint(checkpointId: number): Promise<void> {
|
||||
const body = Buffer.alloc(4);
|
||||
body.writeUInt32LE(checkpointId, 0);
|
||||
await this.sendCommand(Command.CheckpointDelete, body);
|
||||
|
||||
// Remove from local tracking
|
||||
this.breakpoints.delete(checkpointId);
|
||||
this.checkpoints.delete(checkpointId);
|
||||
}
|
||||
|
||||
listBreakpoints(): BreakpointInfo[] {
|
||||
return Array.from(this.breakpoints.values());
|
||||
listBreakpoints(): CheckpointInfo[] {
|
||||
return Array.from(this.checkpoints.values()).filter((cp) => cp.type === "exec");
|
||||
}
|
||||
|
||||
listWatchpoints(): CheckpointInfo[] {
|
||||
return Array.from(this.checkpoints.values()).filter((cp) => cp.type !== "exec");
|
||||
}
|
||||
|
||||
listCheckpoints(): CheckpointInfo[] {
|
||||
return Array.from(this.checkpoints.values());
|
||||
}
|
||||
|
||||
// Snapshot methods
|
||||
async saveSnapshot(filename: string): Promise<void> {
|
||||
const filenameBuffer = Buffer.from(filename, "utf8");
|
||||
const body = Buffer.alloc(1 + filenameBuffer.length);
|
||||
body[0] = filenameBuffer.length;
|
||||
filenameBuffer.copy(body, 1);
|
||||
await this.sendCommand(Command.Dump, body);
|
||||
}
|
||||
|
||||
async loadSnapshot(filename: string): Promise<void> {
|
||||
const filenameBuffer = Buffer.from(filename, "utf8");
|
||||
const body = Buffer.alloc(1 + filenameBuffer.length);
|
||||
body[0] = filenameBuffer.length;
|
||||
filenameBuffer.copy(body, 1);
|
||||
await this.sendCommand(Command.Undump, body);
|
||||
}
|
||||
|
||||
// Autostart a program
|
||||
async autostart(filename: string, fileIndex = 0, runAfterLoad = true): Promise<void> {
|
||||
const filenameBuffer = Buffer.from(filename, "utf8");
|
||||
// Body: run(1) + index(2) + filename_length(1) + filename
|
||||
const body = Buffer.alloc(4 + filenameBuffer.length);
|
||||
body[0] = runAfterLoad ? 1 : 0;
|
||||
body.writeUInt16LE(fileIndex, 1);
|
||||
body[3] = filenameBuffer.length;
|
||||
filenameBuffer.copy(body, 4);
|
||||
await this.sendCommand(Command.AutoStart, body);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,41 @@ export const API_VERSION = 0x02;
|
|||
|
||||
// Command codes
|
||||
export enum Command {
|
||||
// Memory operations
|
||||
MemoryGet = 0x01,
|
||||
MemorySet = 0x02,
|
||||
CheckpointSet = 0x11,
|
||||
|
||||
// Checkpoint (breakpoint/watchpoint) operations
|
||||
CheckpointGet = 0x11,
|
||||
CheckpointSet = 0x12,
|
||||
CheckpointDelete = 0x13,
|
||||
RegistersGet = 0x22,
|
||||
Continue = 0x31,
|
||||
Step = 0x32,
|
||||
CheckpointList = 0x14,
|
||||
CheckpointToggle = 0x15,
|
||||
|
||||
// Register operations
|
||||
RegistersGet = 0x31,
|
||||
RegistersSet = 0x32,
|
||||
|
||||
// Execution control
|
||||
Dump = 0x41,
|
||||
Undump = 0x42,
|
||||
Reset = 0x43,
|
||||
Exit = 0x71,
|
||||
|
||||
// Resources
|
||||
ResourceGet = 0x51,
|
||||
ResourceSet = 0x52,
|
||||
|
||||
// Advanced execution
|
||||
AdvanceInstructions = 0x71,
|
||||
KeyboardFeed = 0x72,
|
||||
Continue = 0x81,
|
||||
Step = 0x82,
|
||||
|
||||
// Quit
|
||||
Quit = 0xbb,
|
||||
|
||||
// Autostart
|
||||
AutoStart = 0xdd,
|
||||
}
|
||||
|
||||
// Response types
|
||||
|
|
|
|||
398
src/utils/disasm.ts
Normal file
398
src/utils/disasm.ts
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
// 6502 Disassembler
|
||||
|
||||
// Opcode table: [mnemonic, addressing mode, bytes]
|
||||
type AddressingMode =
|
||||
| "impl" // Implied
|
||||
| "acc" // Accumulator
|
||||
| "imm" // Immediate #$xx
|
||||
| "zp" // Zero Page $xx
|
||||
| "zpx" // Zero Page,X $xx,X
|
||||
| "zpy" // Zero Page,Y $xx,Y
|
||||
| "abs" // Absolute $xxxx
|
||||
| "abx" // Absolute,X $xxxx,X
|
||||
| "aby" // Absolute,Y $xxxx,Y
|
||||
| "ind" // Indirect ($xxxx)
|
||||
| "izx" // Indexed Indirect ($xx,X)
|
||||
| "izy" // Indirect Indexed ($xx),Y
|
||||
| "rel"; // Relative (branches)
|
||||
|
||||
type OpcodeEntry = [string, AddressingMode, number];
|
||||
|
||||
// 6502 instruction table
|
||||
const OPCODES: Record<number, OpcodeEntry> = {
|
||||
// ADC
|
||||
0x69: ["ADC", "imm", 2],
|
||||
0x65: ["ADC", "zp", 2],
|
||||
0x75: ["ADC", "zpx", 2],
|
||||
0x6d: ["ADC", "abs", 3],
|
||||
0x7d: ["ADC", "abx", 3],
|
||||
0x79: ["ADC", "aby", 3],
|
||||
0x61: ["ADC", "izx", 2],
|
||||
0x71: ["ADC", "izy", 2],
|
||||
|
||||
// AND
|
||||
0x29: ["AND", "imm", 2],
|
||||
0x25: ["AND", "zp", 2],
|
||||
0x35: ["AND", "zpx", 2],
|
||||
0x2d: ["AND", "abs", 3],
|
||||
0x3d: ["AND", "abx", 3],
|
||||
0x39: ["AND", "aby", 3],
|
||||
0x21: ["AND", "izx", 2],
|
||||
0x31: ["AND", "izy", 2],
|
||||
|
||||
// ASL
|
||||
0x0a: ["ASL", "acc", 1],
|
||||
0x06: ["ASL", "zp", 2],
|
||||
0x16: ["ASL", "zpx", 2],
|
||||
0x0e: ["ASL", "abs", 3],
|
||||
0x1e: ["ASL", "abx", 3],
|
||||
|
||||
// Branches
|
||||
0x90: ["BCC", "rel", 2],
|
||||
0xb0: ["BCS", "rel", 2],
|
||||
0xf0: ["BEQ", "rel", 2],
|
||||
0x30: ["BMI", "rel", 2],
|
||||
0xd0: ["BNE", "rel", 2],
|
||||
0x10: ["BPL", "rel", 2],
|
||||
0x50: ["BVC", "rel", 2],
|
||||
0x70: ["BVS", "rel", 2],
|
||||
|
||||
// BIT
|
||||
0x24: ["BIT", "zp", 2],
|
||||
0x2c: ["BIT", "abs", 3],
|
||||
|
||||
// BRK
|
||||
0x00: ["BRK", "impl", 1],
|
||||
|
||||
// Clear flags
|
||||
0x18: ["CLC", "impl", 1],
|
||||
0xd8: ["CLD", "impl", 1],
|
||||
0x58: ["CLI", "impl", 1],
|
||||
0xb8: ["CLV", "impl", 1],
|
||||
|
||||
// CMP
|
||||
0xc9: ["CMP", "imm", 2],
|
||||
0xc5: ["CMP", "zp", 2],
|
||||
0xd5: ["CMP", "zpx", 2],
|
||||
0xcd: ["CMP", "abs", 3],
|
||||
0xdd: ["CMP", "abx", 3],
|
||||
0xd9: ["CMP", "aby", 3],
|
||||
0xc1: ["CMP", "izx", 2],
|
||||
0xd1: ["CMP", "izy", 2],
|
||||
|
||||
// CPX
|
||||
0xe0: ["CPX", "imm", 2],
|
||||
0xe4: ["CPX", "zp", 2],
|
||||
0xec: ["CPX", "abs", 3],
|
||||
|
||||
// CPY
|
||||
0xc0: ["CPY", "imm", 2],
|
||||
0xc4: ["CPY", "zp", 2],
|
||||
0xcc: ["CPY", "abs", 3],
|
||||
|
||||
// DEC
|
||||
0xc6: ["DEC", "zp", 2],
|
||||
0xd6: ["DEC", "zpx", 2],
|
||||
0xce: ["DEC", "abs", 3],
|
||||
0xde: ["DEC", "abx", 3],
|
||||
|
||||
// DEX, DEY
|
||||
0xca: ["DEX", "impl", 1],
|
||||
0x88: ["DEY", "impl", 1],
|
||||
|
||||
// EOR
|
||||
0x49: ["EOR", "imm", 2],
|
||||
0x45: ["EOR", "zp", 2],
|
||||
0x55: ["EOR", "zpx", 2],
|
||||
0x4d: ["EOR", "abs", 3],
|
||||
0x5d: ["EOR", "abx", 3],
|
||||
0x59: ["EOR", "aby", 3],
|
||||
0x41: ["EOR", "izx", 2],
|
||||
0x51: ["EOR", "izy", 2],
|
||||
|
||||
// INC
|
||||
0xe6: ["INC", "zp", 2],
|
||||
0xf6: ["INC", "zpx", 2],
|
||||
0xee: ["INC", "abs", 3],
|
||||
0xfe: ["INC", "abx", 3],
|
||||
|
||||
// INX, INY
|
||||
0xe8: ["INX", "impl", 1],
|
||||
0xc8: ["INY", "impl", 1],
|
||||
|
||||
// JMP
|
||||
0x4c: ["JMP", "abs", 3],
|
||||
0x6c: ["JMP", "ind", 3],
|
||||
|
||||
// JSR
|
||||
0x20: ["JSR", "abs", 3],
|
||||
|
||||
// LDA
|
||||
0xa9: ["LDA", "imm", 2],
|
||||
0xa5: ["LDA", "zp", 2],
|
||||
0xb5: ["LDA", "zpx", 2],
|
||||
0xad: ["LDA", "abs", 3],
|
||||
0xbd: ["LDA", "abx", 3],
|
||||
0xb9: ["LDA", "aby", 3],
|
||||
0xa1: ["LDA", "izx", 2],
|
||||
0xb1: ["LDA", "izy", 2],
|
||||
|
||||
// LDX
|
||||
0xa2: ["LDX", "imm", 2],
|
||||
0xa6: ["LDX", "zp", 2],
|
||||
0xb6: ["LDX", "zpy", 2],
|
||||
0xae: ["LDX", "abs", 3],
|
||||
0xbe: ["LDX", "aby", 3],
|
||||
|
||||
// LDY
|
||||
0xa0: ["LDY", "imm", 2],
|
||||
0xa4: ["LDY", "zp", 2],
|
||||
0xb4: ["LDY", "zpx", 2],
|
||||
0xac: ["LDY", "abs", 3],
|
||||
0xbc: ["LDY", "abx", 3],
|
||||
|
||||
// LSR
|
||||
0x4a: ["LSR", "acc", 1],
|
||||
0x46: ["LSR", "zp", 2],
|
||||
0x56: ["LSR", "zpx", 2],
|
||||
0x4e: ["LSR", "abs", 3],
|
||||
0x5e: ["LSR", "abx", 3],
|
||||
|
||||
// NOP
|
||||
0xea: ["NOP", "impl", 1],
|
||||
|
||||
// ORA
|
||||
0x09: ["ORA", "imm", 2],
|
||||
0x05: ["ORA", "zp", 2],
|
||||
0x15: ["ORA", "zpx", 2],
|
||||
0x0d: ["ORA", "abs", 3],
|
||||
0x1d: ["ORA", "abx", 3],
|
||||
0x19: ["ORA", "aby", 3],
|
||||
0x01: ["ORA", "izx", 2],
|
||||
0x11: ["ORA", "izy", 2],
|
||||
|
||||
// Stack
|
||||
0x48: ["PHA", "impl", 1],
|
||||
0x08: ["PHP", "impl", 1],
|
||||
0x68: ["PLA", "impl", 1],
|
||||
0x28: ["PLP", "impl", 1],
|
||||
|
||||
// ROL
|
||||
0x2a: ["ROL", "acc", 1],
|
||||
0x26: ["ROL", "zp", 2],
|
||||
0x36: ["ROL", "zpx", 2],
|
||||
0x2e: ["ROL", "abs", 3],
|
||||
0x3e: ["ROL", "abx", 3],
|
||||
|
||||
// ROR
|
||||
0x6a: ["ROR", "acc", 1],
|
||||
0x66: ["ROR", "zp", 2],
|
||||
0x76: ["ROR", "zpx", 2],
|
||||
0x6e: ["ROR", "abs", 3],
|
||||
0x7e: ["ROR", "abx", 3],
|
||||
|
||||
// RTI, RTS
|
||||
0x40: ["RTI", "impl", 1],
|
||||
0x60: ["RTS", "impl", 1],
|
||||
|
||||
// SBC
|
||||
0xe9: ["SBC", "imm", 2],
|
||||
0xe5: ["SBC", "zp", 2],
|
||||
0xf5: ["SBC", "zpx", 2],
|
||||
0xed: ["SBC", "abs", 3],
|
||||
0xfd: ["SBC", "abx", 3],
|
||||
0xf9: ["SBC", "aby", 3],
|
||||
0xe1: ["SBC", "izx", 2],
|
||||
0xf1: ["SBC", "izy", 2],
|
||||
|
||||
// Set flags
|
||||
0x38: ["SEC", "impl", 1],
|
||||
0xf8: ["SED", "impl", 1],
|
||||
0x78: ["SEI", "impl", 1],
|
||||
|
||||
// STA
|
||||
0x85: ["STA", "zp", 2],
|
||||
0x95: ["STA", "zpx", 2],
|
||||
0x8d: ["STA", "abs", 3],
|
||||
0x9d: ["STA", "abx", 3],
|
||||
0x99: ["STA", "aby", 3],
|
||||
0x81: ["STA", "izx", 2],
|
||||
0x91: ["STA", "izy", 2],
|
||||
|
||||
// STX
|
||||
0x86: ["STX", "zp", 2],
|
||||
0x96: ["STX", "zpy", 2],
|
||||
0x8e: ["STX", "abs", 3],
|
||||
|
||||
// STY
|
||||
0x84: ["STY", "zp", 2],
|
||||
0x94: ["STY", "zpx", 2],
|
||||
0x8c: ["STY", "abs", 3],
|
||||
|
||||
// Transfers
|
||||
0xaa: ["TAX", "impl", 1],
|
||||
0xa8: ["TAY", "impl", 1],
|
||||
0xba: ["TSX", "impl", 1],
|
||||
0x8a: ["TXA", "impl", 1],
|
||||
0x9a: ["TXS", "impl", 1],
|
||||
0x98: ["TYA", "impl", 1],
|
||||
};
|
||||
|
||||
export interface DisassembledInstruction {
|
||||
address: number;
|
||||
addressHex: string;
|
||||
bytes: number[];
|
||||
bytesHex: string;
|
||||
mnemonic: string;
|
||||
operand: string;
|
||||
fullInstruction: string;
|
||||
size: number;
|
||||
// For branches: the target address
|
||||
branchTarget?: number;
|
||||
branchTargetHex?: string;
|
||||
}
|
||||
|
||||
export function disassemble(
|
||||
data: Buffer | number[],
|
||||
startAddress: number,
|
||||
count?: number
|
||||
): DisassembledInstruction[] {
|
||||
const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
const result: DisassembledInstruction[] = [];
|
||||
let offset = 0;
|
||||
let instructionCount = 0;
|
||||
|
||||
while (offset < bytes.length) {
|
||||
if (count !== undefined && instructionCount >= count) break;
|
||||
|
||||
const address = startAddress + offset;
|
||||
const opcode = bytes[offset];
|
||||
const entry = OPCODES[opcode];
|
||||
|
||||
if (!entry) {
|
||||
// Unknown opcode - treat as single byte data
|
||||
result.push({
|
||||
address,
|
||||
addressHex: `$${address.toString(16).padStart(4, "0")}`,
|
||||
bytes: [opcode],
|
||||
bytesHex: opcode.toString(16).padStart(2, "0"),
|
||||
mnemonic: "???",
|
||||
operand: `$${opcode.toString(16).padStart(2, "0")}`,
|
||||
fullInstruction: `??? $${opcode.toString(16).padStart(2, "0")}`,
|
||||
size: 1,
|
||||
});
|
||||
offset++;
|
||||
instructionCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const [mnemonic, mode, size] = entry;
|
||||
|
||||
// Check if we have enough bytes
|
||||
if (offset + size > bytes.length) break;
|
||||
|
||||
const instrBytes: number[] = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
instrBytes.push(bytes[offset + i]);
|
||||
}
|
||||
|
||||
const bytesHex = instrBytes.map((b) => b.toString(16).padStart(2, "0")).join(" ");
|
||||
let operand = "";
|
||||
let branchTarget: number | undefined;
|
||||
let branchTargetHex: string | undefined;
|
||||
|
||||
switch (mode) {
|
||||
case "impl":
|
||||
operand = "";
|
||||
break;
|
||||
case "acc":
|
||||
operand = "A";
|
||||
break;
|
||||
case "imm":
|
||||
operand = `#$${bytes[offset + 1].toString(16).padStart(2, "0")}`;
|
||||
break;
|
||||
case "zp":
|
||||
operand = `$${bytes[offset + 1].toString(16).padStart(2, "0")}`;
|
||||
break;
|
||||
case "zpx":
|
||||
operand = `$${bytes[offset + 1].toString(16).padStart(2, "0")},X`;
|
||||
break;
|
||||
case "zpy":
|
||||
operand = `$${bytes[offset + 1].toString(16).padStart(2, "0")},Y`;
|
||||
break;
|
||||
case "abs":
|
||||
operand = `$${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")}`;
|
||||
break;
|
||||
case "abx":
|
||||
operand = `$${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")},X`;
|
||||
break;
|
||||
case "aby":
|
||||
operand = `$${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")},Y`;
|
||||
break;
|
||||
case "ind":
|
||||
operand = `($${(bytes[offset + 1] | (bytes[offset + 2] << 8)).toString(16).padStart(4, "0")})`;
|
||||
break;
|
||||
case "izx":
|
||||
operand = `($${bytes[offset + 1].toString(16).padStart(2, "0")},X)`;
|
||||
break;
|
||||
case "izy":
|
||||
operand = `($${bytes[offset + 1].toString(16).padStart(2, "0")}),Y`;
|
||||
break;
|
||||
case "rel": {
|
||||
// Relative branch - calculate target address
|
||||
const displacement = bytes[offset + 1];
|
||||
// Convert to signed
|
||||
const signed = displacement > 127 ? displacement - 256 : displacement;
|
||||
branchTarget = (address + 2 + signed) & 0xffff;
|
||||
branchTargetHex = `$${branchTarget.toString(16).padStart(4, "0")}`;
|
||||
operand = branchTargetHex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const fullInstruction = operand ? `${mnemonic} ${operand}` : mnemonic;
|
||||
|
||||
result.push({
|
||||
address,
|
||||
addressHex: `$${address.toString(16).padStart(4, "0")}`,
|
||||
bytes: instrBytes,
|
||||
bytesHex,
|
||||
mnemonic,
|
||||
operand,
|
||||
fullInstruction,
|
||||
size,
|
||||
branchTarget,
|
||||
branchTargetHex,
|
||||
});
|
||||
|
||||
offset += size;
|
||||
instructionCount++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Common C64 KERNAL/BASIC entry points for label hints
|
||||
export const KERNAL_LABELS: Record<number, string> = {
|
||||
0xffd2: "CHROUT",
|
||||
0xffe4: "GETIN",
|
||||
0xffcf: "CHRIN",
|
||||
0xffc0: "OPEN",
|
||||
0xffc3: "CLOSE",
|
||||
0xffc6: "CHKIN",
|
||||
0xffc9: "CHKOUT",
|
||||
0xffcc: "CLRCHN",
|
||||
0xffd5: "LOAD",
|
||||
0xffd8: "SAVE",
|
||||
0xe544: "CLRSCR",
|
||||
0xa871: "CHRGET",
|
||||
0xbdcd: "FLTASC",
|
||||
0xb7f7: "FMULT",
|
||||
0xb850: "FDIV",
|
||||
0xb867: "MOVFM",
|
||||
0xbba2: "GIVAYF",
|
||||
};
|
||||
|
||||
export function getLabelForAddress(address: number): string | undefined {
|
||||
return KERNAL_LABELS[address];
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from "./c64.js";
|
||||
export * from "./disasm.js";
|
||||
|
|
|
|||
Loading…
Reference in a new issue