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:
Simen Svale 2025-12-30 02:04:23 +01:00
parent 56ea0984bb
commit cebe6f4a14
5 changed files with 989 additions and 17 deletions

View file

@ -11,6 +11,8 @@ import {
getVideoAddresses, getVideoAddresses,
getGraphicsMode, getGraphicsMode,
isSpriteVisible, isSpriteVisible,
disassemble,
getLabelForAddress,
} from "./utils/index.js"; } from "./utils/index.js";
const server = new McpServer({ const server = new McpServer({
@ -644,8 +646,8 @@ Related tools: setBreakpoint, deleteBreakpoint`,
breakpoints: breakpoints.map((bp) => ({ breakpoints: breakpoints.map((bp) => ({
id: bp.id, id: bp.id,
address: { address: {
value: bp.address, value: bp.startAddress,
hex: `$${bp.address.toString(16).padStart(4, "0")}`, hex: `$${bp.startAddress.toString(16).padStart(4, "0")}`,
}, },
enabled: bp.enabled, enabled: bp.enabled,
temporary: bp.temporary, 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 // SEMANTIC LAYER TOOLS - Interpreted output for autonomous debugging
// ============================================================================= // =============================================================================

View file

@ -7,6 +7,7 @@ import {
ResponseType, ResponseType,
ErrorCode, ErrorCode,
MemorySpace, MemorySpace,
CheckpointOp,
ViceResponse, ViceResponse,
ConnectionState, ConnectionState,
} from "./types.js"; } from "./types.js";
@ -18,13 +19,20 @@ export interface ViceError {
suggestion?: string; suggestion?: string;
} }
export interface BreakpointInfo { export type CheckpointType = "exec" | "load" | "store";
export interface CheckpointInfo {
id: number; id: number;
address: number; startAddress: number;
endAddress: number;
enabled: boolean; enabled: boolean;
temporary: boolean; temporary: boolean;
type: CheckpointType;
} }
// Keep for backwards compatibility
export type BreakpointInfo = CheckpointInfo;
export class ViceClient { export class ViceClient {
private socket: Socket | null = null; private socket: Socket | null = null;
private requestId = 0; private requestId = 0;
@ -42,8 +50,8 @@ export class ViceClient {
port: 0, port: 0,
running: true, running: true,
}; };
// Track breakpoints locally (VICE doesn't have a reliable list command in all versions) // Track checkpoints locally (VICE doesn't have a reliable list command in all versions)
private breakpoints = new Map<number, BreakpointInfo>(); private checkpoints = new Map<number, CheckpointInfo>();
// Event handlers for async events (breakpoints, etc.) // Event handlers for async events (breakpoints, etc.)
public onStopped?: (response: ViceResponse) => void; public onStopped?: (response: ViceResponse) => void;
@ -376,6 +384,34 @@ export class ViceClient {
return this.sendCommand(Command.RegistersGet, body); 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> { async continue(): Promise<void> {
await this.sendCommand(Command.Continue); await this.sendCommand(Command.Continue);
this.state.running = true; this.state.running = true;
@ -390,6 +426,14 @@ export class ViceClient {
return response; 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> { async reset(hard = false): Promise<void> {
const body = Buffer.alloc(1); const body = Buffer.alloc(1);
body[0] = hard ? 1 : 0; body[0] = hard ? 1 : 0;
@ -420,34 +464,159 @@ export class ViceClient {
body.writeUInt16LE(address, 2); body.writeUInt16LE(address, 2);
body[4] = stop ? 1 : 0; body[4] = stop ? 1 : 0;
body[5] = enabled ? 1 : 0; body[5] = enabled ? 1 : 0;
body[6] = 0x01; // Exec body[6] = CheckpointOp.Exec;
body[7] = temporary ? 1 : 0; body[7] = temporary ? 1 : 0;
const response = await this.sendCommand(Command.CheckpointSet, body); const response = await this.sendCommand(Command.CheckpointSet, body);
const id = response.body.readUInt32LE(0); const id = response.body.readUInt32LE(0);
// Track locally // Track locally
this.breakpoints.set(id, { this.checkpoints.set(id, {
id, id,
address, startAddress: address,
endAddress: address,
enabled, enabled,
temporary, temporary,
type: "exec",
}); });
return id; 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> { async deleteBreakpoint(checkpointId: number): Promise<void> {
const body = Buffer.alloc(4); const body = Buffer.alloc(4);
body.writeUInt32LE(checkpointId, 0); body.writeUInt32LE(checkpointId, 0);
await this.sendCommand(Command.CheckpointDelete, body); await this.sendCommand(Command.CheckpointDelete, body);
// Remove from local tracking // Remove from local tracking
this.breakpoints.delete(checkpointId); this.checkpoints.delete(checkpointId);
} }
listBreakpoints(): BreakpointInfo[] { listBreakpoints(): CheckpointInfo[] {
return Array.from(this.breakpoints.values()); 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);
} }
} }

View file

@ -6,15 +6,41 @@ export const API_VERSION = 0x02;
// Command codes // Command codes
export enum Command { export enum Command {
// Memory operations
MemoryGet = 0x01, MemoryGet = 0x01,
MemorySet = 0x02, MemorySet = 0x02,
CheckpointSet = 0x11,
// Checkpoint (breakpoint/watchpoint) operations
CheckpointGet = 0x11,
CheckpointSet = 0x12,
CheckpointDelete = 0x13, CheckpointDelete = 0x13,
RegistersGet = 0x22, CheckpointList = 0x14,
Continue = 0x31, CheckpointToggle = 0x15,
Step = 0x32,
// Register operations
RegistersGet = 0x31,
RegistersSet = 0x32,
// Execution control
Dump = 0x41,
Undump = 0x42,
Reset = 0x43, 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 // Response types

398
src/utils/disasm.ts Normal file
View 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];
}

View file

@ -1 +1,2 @@
export * from "./c64.js"; export * from "./c64.js";
export * from "./disasm.js";