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,
|
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
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
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 "./c64.js";
|
||||||
|
export * from "./disasm.js";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue