6502 Debugger for dotnet-6502A Visual Studio Code extension for debugging 6502 assembly source and machine code programs using the dotnet-6502 emulator.
Installing the VSCode extension from the MarketplaceThe easiest way to install is directly from the Visual Studio Code Marketplace:
Or via command line:
RequirementsRequirements depend on how you install the extension and what you want to do. Using the extension (Marketplace install + package manager)
For source-level debugging (additional requirement)
For installing the extension from a local .vsix package
For developing the extension from sourceInstalling the VSCode extension from a .vsix packageThis is the simplest way to install the extension locally without setting up a development environment.
To uninstall:
Building and running the VSCode extension from sourceUse this approach if you want to develop or debug the extension itself.
Debug Quick StartFor Source-Level Debugging with ca65 assembler for a C64 program
That's it! The extension creates both the build task and launch configuration for you. Alternative: Generate SeparatelyYou can also generate the build task and launch configuration separately:
Example ConfigurationThe generated files look like this:
Note: No need to specify Features
UsageSource debugging
Disassembly debugging
Or create a manual launch.json configuration for other scenarios (see below).
Manual Launch ConfigurationExample for debugging pre-built .prg files without source. See full configuration reference below for all options.
ConfigurationThere are three ways to use the debugger, each with different launch.json configurations:
Launch Configuration Parameters
Example
|
| Element | Syntax | Example |
|---|---|---|
| Register | A, X, Y, SP, PC (case-insensitive) |
A == $FF |
| Status flag | C, Z, N, V, I, D, B (0 = clear, 1 = set) |
Z == 1 |
| Memory byte | [$addr] |
[$D020] == $01 |
| Indexed memory | [$addr + reg] |
[$0300 + X] > $7F |
| Hex literal | $hex or 0xhex |
$FF, 0xFF |
| Decimal literal | digits | 10, 255 |
Comparison operators: ==, !=, <, <=, >, >=
Logical operators: && (and), || (or) — evaluated left-to-right, short-circuit
Examples:
A == $FF ; Stop only when accumulator is $FF
X >= 10 ; Stop when X register is 10 or more
Z == 1 ; Stop when Zero flag is set
C == 0 ; Stop when Carry flag is clear
[$D020] == $01 ; Stop when border colour register equals 1
[$0300 + X] > $7F ; Stop when indexed memory byte is > 127
A == $FF && X == 0 ; Both conditions must be true
A == $00 || A == $FF ; Stop when A is either 0 or 255
PC == $C080 ; Stop when PC reaches a specific address
Notes:
- Register and flag names are case-insensitive (
a == $ffis the same asA == $FF) - Flags are treated as integers:
1= set,0= clear - Memory addresses wrap at the 64 KB boundary
- If the expression cannot be parsed, the debugger always stops (fail-safe)
Logpoints
Logpoints let you print messages to the Debug Console without pausing execution — like printf debugging without modifying your code.
Setting a logpoint:
- Set a breakpoint by clicking the gutter
- Right-click the breakpoint dot → "Edit Breakpoint..."
- Change the dropdown from "Expression" to "Log Message"
- Type your message and press Enter
The breakpoint dot turns into a diamond shape to indicate a logpoint.
Expression interpolation:
Use {expr} inside the message to evaluate expressions. The same values available in Watch/Hover are supported:
| Placeholder | Evaluates to | Example output |
|---|---|---|
{A} |
Accumulator (hex) | $FF |
{X}, {Y} |
X/Y register (hex) | $0A |
{PC} |
Program counter (hex) | $C012 |
{SP} |
Stack pointer (hex) | $FB |
{C}, {Z}, {N}, {V}, {I}, {D} |
Flag (0 or 1) | 1 |
{$C000}, {0xC000} |
Memory byte at address (hex) | $42 |
{symbolname} |
ca65 symbol address + value | $C000[$42] |
Examples:
Loop iteration: A={A} X={X}
Border color is {$D020}
Screen pointer at {screenptr}
PC={PC} SP={SP} flags: C={C} Z={Z} N={N}
Notes:
- Logpoints can also have a condition — the message is only logged when the condition is true
- Unrecognized
{expr}placeholders are left as-is in the output - Logpoints work with source breakpoints, instruction breakpoints, and function breakpoints
Hit Count Breakpoints
Hit count breakpoints let you stop (or log) only after a breakpoint has been reached a certain number of times — useful for breaking on the Nth iteration of a loop.
Setting a hit count:
- Set a breakpoint by clicking the gutter
- Right-click the breakpoint dot → "Edit Breakpoint..."
- Change the dropdown to "Hit Count"
- Type a hit count expression and press Enter
Supported syntax:
| Expression | Meaning | Example |
|---|---|---|
N |
Break when hit count equals N | 5 — break on the 5th hit |
= N |
Break when hit count equals N (explicit) | = 10 |
>= N |
Break when hit count is N or more | >= 3 |
> N |
Break when hit count exceeds N | > 100 |
% N |
Break on every Nth hit (modulo) | % 10 — break every 10th hit |
Examples:
5 ; Break on exactly the 5th hit
>= 100 ; Break from the 100th hit onward
% 8 ; Break every 8th hit
= 1 ; Break on the first hit only (same as a normal breakpoint)
Notes:
- Hit counts reset when the debug session is restarted or when breakpoints are reconfigured
- Hit counts combine with expression conditions — the hit count only increments when the expression condition passes
- Hit counts combine with logpoints — a logpoint with a hit count only logs on matching hits
- Hit count breakpoints work with source breakpoints, instruction breakpoints, and function breakpoints
Source-Line Stepping
In 6502 assembly, a single source line usually maps to one CPU instruction. But with ca65 macros, one source line (the macro invocation) can expand to multiple instructions. Without source-line stepping, pressing F10/F11 would advance by one instruction at a time, requiring multiple key presses to get past a macro call.
The debugger automatically detects multi-instruction source lines and steps through them in one action:
| Context | F10 (Step Over) | F11 (Step In) |
|---|---|---|
| Normal instruction (1:1 mapping) | Advance one instruction | Advance one instruction |
| Macro invocation (N instructions on one line) | Execute all N instructions, stop at next source line | Execute until a JSR enters a subroutine (stops at entry), or until the next source line |
| JSR inside a macro (step-over) | Step over the JSR and continue through remaining same-line instructions | Enter the subroutine |
Disassembly view (instruction granularity) |
Always one instruction | Always one instruction |
How it works:
- When you press F10/F11 from the source editor, the debugger looks up all addresses mapped to the current source line
- If multiple addresses share the same line (macro expansion), it keeps executing until PC moves to a different source line
- Breakpoints within the same source line are still respected — if an intermediate instruction has a breakpoint, execution stops there
- In the Disassembly view, stepping always advances one instruction regardless of source mapping
Notes:
- Source-line stepping requires a ca65
.dbgfile — without debug symbols, stepping is always instruction-level - Standard single-instruction source lines (the common case) behave identically to before — no performance overhead
- Combines with interrupt skipping: if an IRQ fires during source-line stepping, it is auto-skipped as usual
Interrupt Handling During Stepping
When single-stepping through code on a system like the C64, hardware interrupts (IRQ/NMI) can fire between any two instructions. Without special handling, pressing F10/F11 could land you inside the Kernal's interrupt service routine — deep in ROM code with no source mapping.
By default, the debugger automatically skips these interrupt handlers:
- After each step, it detects if the CPU entered an IRQ or NMI handler
- If the handler has no source mapping (e.g., ROM code), it sets a temporary breakpoint at the return address and continues execution
- When the ISR completes (
RTI), the debugger stops at your original code — as if the interrupt never happened
This makes stepping behave predictably even on interrupt-heavy systems.
When skipping does NOT apply:
- If you've loaded debug symbols for the ISR (e.g., Kernal
.dbgfile viadbgFiles), the debugger lets you step through it normally BRKinstructions are never skipped (they are treated as software breakpoints, not hardware interrupts)
To disable: Set "skipInterrupts": false in your launch configuration if you want to step into every interrupt handler regardless of source mapping.
Stepping Outside Source Code Boundaries
What happens when execution leaves source-mapped addresses:
When your program executes code outside the source-mapped address range (e.g., calling into ROM routines via JSR), the debugger behavior changes:
- Variables View persists: CPU registers (PC, A, X, Y, SP) and flags remain visible and update normally
- Source view disappears: The editor no longer shows a highlighted line since there's no source mapping
- Call Stack shows address: The call stack displays the memory address and disassembled instruction (e.g.,
$C100: c100 0a ASL A) - Warning message: A console warning indicates you're outside program bounds
Example scenario:
JSR $C100 ; Stepping into this jumps outside your source code
; $C100 might contain self-modifying code or ROM routine
Using the Disassembly View
Opening the Disassembly View:
When stopped at an address without source mapping, you can view the raw disassembly:
- Right-click on the Call Stack entry → Select "Open Disassembly View"
- Or use Command Palette (Cmd+Shift+P / Ctrl+Shift+P) → "Debug: Open Disassembly View"
The Disassembly view shows:
- Memory addresses
- Instruction bytes (hex)
- Disassembled mnemonics
- The current instruction pointer highlighted
Stepping through disassembly:
Once the Disassembly view is open, you can continue stepping with F10 (Step Over) or F11 (Step Into). VS Code automatically sends instruction granularity for steps in this view, so each press advances exactly one CPU instruction. The Variables view continues to show register values.
Returning to source code:
When execution returns to your source-mapped address range (e.g., after an RTS instruction), the editor automatically switches back to showing your .asm source file with the highlighted line.
Note: The Debug Adapter Protocol does not (seemingly) provide a way for debug adapters to automatically open the Disassembly view. This is a VS Code UI design decision - users must manually open it the first time they need it. However, once opened, the disassembly view remains visible across debug sessions.
Memory Inspection
Memory Viewer (Primary Method)
The memory viewer opens memory contents in an editor tab with a traditional hex dump format. This is the recommended way to inspect memory ranges:
How to Open:
- Toolbar button: Click the array icon ($(symbol-array)) in the debug toolbar during a debug session
- Command palette: Run "View Memory" command
Usage:
- Enter start address (e.g.,
0x0000,$C000,49152)- Default:
0x0000(start of address space) - Supports hex (0x or $) and decimal formats
- Default:
- Enter end address (e.g.,
0x00FF,$C0FF,255)- Default: 256 bytes from start address (e.g.,
0x00FFif starting at0x0000) - Supports hex (0x or $) and decimal formats
- Default: 256 bytes from start address (e.g.,
Output Format:
Memory is displayed in a read-only editor tab titled with the address range (e.g., "0x0000-0x00FF"):
0x0000: A9 01 85 00 A9 0A 85 01 A9 00 85 02 A5 00 18 65 ................
0x0010: 01 85 02 E6 03 A5 00 C9 FF D0 F0 A5 02 C9 FF D0 ................
Each row shows:
- Memory address (hex)
- 16 bytes in hexadecimal
- ASCII/PETSCII representation (non-printable shown as dots)
Examples:
0x0000to0x00FF- Zero page (256 bytes)0x0100to0x01FF- Stack area (256 bytes)0xC000to0xFFFF- View entire ROM area on C64 (16KB)0xD000to0xD3FF- VIC-II registers on C64 (1KB)0x0000to0xFFFF- Entire 64KB address space
Features:
- No size limits - can view entire 64KB address space
- Results displayed in searchable editor tab
- Tab title shows address range for easy reference
- Addresses respect 64KB boundary (stops at 0xFFFF)
Debug Console dump Command (Alternative Method)
For quick memory inspection without opening a new tab, the Debug Console supports a dump command:
Basic Usage:
dump 0xc000 0xc0ff # Dump from $C000 to $C0FF (end address)
dump 0xc000 256 # Dump 256 bytes starting at $C000 (length)
dump 0xc000 # Dump 256 bytes (default length)
Command Aliases:
dump- Full command namemd- Short alias (memory dump)memdump- Long alias
Parameter Formats:
Second parameter interpretation:
- Decimal number → treated as byte length (e.g.,
dump 0xc000 256) - Hex format (0x or $) → treated as end address (e.g.,
dump $c000 $c0ff)
Examples:
dump 0x0000 256 # Dump zero page and stack area
md fffe 2 # Interrupt vectors
dump $d000 $d3ff # VIC-II registers on C64
Debug Console set Command
Modify registers and memory directly from the Debug Console:
set A $42 # Set accumulator to $42
set PC $C000 # Set program counter to $C000
set X 10 # Set X register to 10 (decimal)
set $C000 $FF # Set memory at $C000 to $FF
Debug Console Expressions
Type expressions directly in the Debug Console to evaluate them:
$C000 # Read memory at $C000
PC # Show current program counter
A # Show accumulator value
screenptr # Resolve ca65 symbol (if .dbg loaded)
Register and Flag Editing
You can modify CPU registers and flags during debugging:
In the Variables panel:
- Double-click any register (PC, A, X, Y, SP) to edit its value
- Double-click any flag (C, Z, I, D, B, V, N) to toggle it (use
0/1ortrue/false) - Values can be entered in hex (
$C000,0xC0) or decimal (192)
In the Debug Console:
set A $42 # Set accumulator to $42
set PC $C000 # Set program counter to $C000
set X 10 # Set X register to 10 (decimal)
set $C000 $FF # Set memory at $C000 to $FF
When you edit the PC register, the editor view automatically updates to show the new location.
Jump to Line (Set PC)
You can jump the Program Counter to any source line without executing the instructions in between:
- Right-click on a line number or right-click in the gutter (to the left of line numbers)
- Select "Jump to Line (Set PC)"
This sets the PC to the address corresponding to that source line. If you click on a non-code line (comment, label, blank line), it automatically snaps to the nearest executable line.
This is available only when the debugger is paused (debugState == 'stopped').
Inline Address Decorations
When debugging with a .dbg file, each source line that maps to a 6502 address shows the address as dim italic text after the line content:
LDA #$01 $C000
STA $D020 $C002
RTS $C005
For macro body lines (where the address depends on the call site), the decoration updates dynamically on each stop to show the actual address for that specific macro invocation.
Hover Evaluation
Hover over values in your source code to see their current state:
- Memory addresses: Hover
$C000or0xC000→ shows the byte value at that address - Immediate values: Hover
#$42→ shows the literal value (not memory contents) - Registers: Hover
A,X,Y,PC,SP→ shows current register value - ca65 symbols: Hover a label name (e.g.,
screenptr) → shows the symbol's address and memory contents
Symbol Resolution
When a .dbg file is loaded, ca65 symbols (labels and equates) can be evaluated:
- In the Debug Console: Type a symbol name (e.g.,
screenptr) to see its address and value - In the Watch panel: Add symbol names as watch expressions
- On Hover: Hover over symbol names in source code
Labels show their address and the memory byte at that address. Equates show their numeric value.
Multi-File Debug Symbols
For debugging programs that interact with ROM code (e.g., C64 KERNAL), you can merge multiple .dbg files:
{
"type": "dotnet6502",
"request": "attach",
"name": "Debug with ROM symbols",
"preLaunchTask": "Build my-program.asm (C64)",
"dbgFiles": [
"${userHome}/path/to/rom.dbg"
],
"stopOnEntry": true
}
The primary .dbg file is auto-detected from the program path (or specified via dbgFile). The dbgFiles array adds additional symbol sources that are merged together.
Limitations
- Variable inspection limited to registers, flags, and memory addresses
- Disassembly view does not open automatically (must be opened manually via right-click on Call Stack)
How Emulator Mode Works
Note: Currently only the Avalonia Desktop app (
Highbyte.DotNet6502.App.Avalonia.Desktop) is supported as an emulator host.
When using "debugAdapter": "emulator" (launch) or "request": "attach":
- The emulator host starts with:
--enableExternalDebug --debug-port 6502 --system C64 --start --waitForSystemReady --loadPrg <path> - The emulator starts the specified system (e.g., C64)
- Waits for the system to be ready (BASIC prompt appears)
- Loads the PRG file into memory at the address specified in the file
- Optionally runs the program by setting the CPU PC to the load address
- VSCode connects the debugger via TCP
In launch (emulator) mode, steps 1-5 are handled automatically by the extension. In attach mode, you start the emulator manually (or use the UI toggle — see Attach mode examples above) and the extension only does step 6.
Note: Set runProgram: true if you want the program to start automatically. Otherwise, the program is loaded but you'll need to manually start it (e.g., SYS 49152 in C64 BASIC, or step through with the debugger).
Advanced: Debugging the C# Code
Window 1 - Extension Test (6502 Debugging):
- Open
/tools/vscode-extension-test/folder in VS Code - Open your 6502 test program (e.g.,
test-program.asm) - Press F5 to start debugging the 6502 program
- The debug adapter process will start in the background
Window 2 - Debugging the C# Code:
- Open the repository root folder in a separate VS Code window
- Set breakpoints in the C# code you want to debug:
src/apps/Highbyte.DotNet6502.DebugAdapter/DebugAdapterLogic.cs- Debug adapter protocol handlingsrc/libraries/Highbyte.DotNet6502/CPU.cs- 6502 CPU emulation- Any other emulator code
- Go to Run & Debug → "Attach to DotNet6502 VSCode Debug Adapter"
- Press F5 to attach to the running debug adapter
Now when you step through 6502 instructions in Window 1, VS Code will hit your C# breakpoints in Window 2, allowing you to:
- See exactly how each 6502 instruction is executed
- Debug the debug adapter protocol implementation
- Step through the emulator's internal logic
- Inspect memory, CPU state, and other internals
Tips:
- Set breakpoints in
HandleNextAsync()orHandleContinueAsync()inDebugAdapterLogic.csto catch every step - Set breakpoints in
CPU.Execute()to see each 6502 instruction execution - Use the Variables panel in Window 2 to inspect the emulator's internal state