diff --git a/src/index.ts b/src/index.ts index a51f969..13aaf31 100644 --- a/src/index.ts +++ b/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 // ============================================================================= diff --git a/src/protocol/client.ts b/src/protocol/client.ts index 8d5de39..f16d796 100644 --- a/src/protocol/client.ts +++ b/src/protocol/client.ts @@ -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(); + // Track checkpoints locally (VICE doesn't have a reliable list command in all versions) + private checkpoints = new Map(); // 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 { + // 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 { 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 { + 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 { 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 { + 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 { + 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 { 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 { + 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 { + 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 { + 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); } } diff --git a/src/protocol/types.ts b/src/protocol/types.ts index 42c9a26..f4d98af 100644 --- a/src/protocol/types.ts +++ b/src/protocol/types.ts @@ -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 diff --git a/src/utils/disasm.ts b/src/utils/disasm.ts new file mode 100644 index 0000000..dd32c41 --- /dev/null +++ b/src/utils/disasm.ts @@ -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 = { + // 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 = { + 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]; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 8467dc6..edd30e7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ export * from "./c64.js"; +export * from "./disasm.js";