LEVENT KAYA'S WEBSITE

CONTACT    ARCHIVE    RSS     DONATE


Graphical Integrated Debugging and Emulation Suite for 6502 CPU in C

· tags: c, 6502, emulator, debugger, reverse-engineering, and low-level

comp

Hello everyone, in this blog post I will be explaining how and why I wrote c6502, an integrated debugging and emulation suite that I developed for the MOS 6502.


A little bit of tech history

The MOS 6502 is one of the most influential microprocessors in computing history. Introduced in 1975 by MOS Technology, the 6502 was designed to be a low-cost, high-performance alternative to the more expensive processors of the time, such as Intel’s 8080 and Motorola’s 6800.

The 6502 found its way into iconic devices like the Apple I/II, the Commodore PET, and the Atari 2600, fueling the growth of personal computing and gaming. Its simple yet powerful instruction set made it an excellent learning tool for budding engineers and programmers.


Why?

When I started tinkering with 6502 assembly, I quickly realized there wasn’t a single tool that combined all the features I needed. There were great emulators, solid debuggers, and even some decompilers — but they were all separate. Switching between programs disrupted the workflow. So, I created one :)


The goal

My biggest goal with c6502 was to actually run assembly and binaries written for the MOS 6502. I used the cc65 toolchain (ca65 assembler and ld65 linker) to build binaries, then fed them into c6502.

A real 6502 code and binary that runs assembled on c6502:

bin

Another goal was to monitor memory and registers in real time — also supported.


Features overview


The emulation process

In c6502, the emulation process closely mimics the behavior of the 6502:

1. Resetting the CPU

In c_reset() the registers (A, X, Y, PC, SP) and flags are initialized. Acc, X, Y → 0; PC → 0x0000; flags cleared.

2. Fetching the instruction

uint8_t opcode = memory->mem[cpu->reg.pc];
printf("PC: 0x%04x, Opcode: 0x%02x\n", cpu->reg.pc, opcode);

3. Decoding the instruction

for (size_t i = 0; i < set_size; i++) {
  if (opcode == instruction_set[i].opcode) {
    instruction = &instruction_set[i];
    break;
  }
}

4. Addressing modes

Addressing mode determines how to obtain operands (immediate/absolute/zero page/etc.). Example absolute:

uint16_t address = ((mem->mem[cpu->reg.pc + 2] << 8) | mem->mem[cpu->reg.pc + 1]);

5. Executing instructions

Example LDA handler:

void lda_handler(c_cpu_t *cpu, m_memory_t *mem, uint16_t address)
{
  cpu->reg.acc = mem->mem[address];
  if (cpu->reg.acc == 0) { SET_FLAG(cpu->reg, FLAG_ZERO); }
  else { CLEAR_FLAG(cpu->reg, FLAG_ZERO); }
  if (cpu->reg.acc & 0x80) { SET_FLAG(cpu->reg, FLAG_NEGATIVE); }
  else { CLEAR_FLAG(cpu->reg, FLAG_NEGATIVE); }
}

6. Putting all together

Reset → fetch → decode → address → execute, looped—mirroring the original hardware while exposing a transparent, real-time debugging view.


The c6502 User Interface

ui

The GUI keeps everything in one place for emulation, debugging, and reverse engineering.

  1. Virtual interface — Main display area for visual output.
  2. Disassembler — Live disassembly that follows execution and highlights the current instruction.
  3. Register status — Real-time PC, SP, A, X, Y and flags view.
  4. Memory status — Inspect memory values and how instructions affect them in real time.

Overall Design Philosophy

All critical info at a glance without switching tools — ideal for learning, debugging, and analysis.


Conclusion

Developing c6502 merged my love for retrocomputing with low-level programming and tool building. The result is a comprehensive suite that integrates emulation, debugging, and decompilation into a single, user-friendly tool. If you’re interested in trying it, grab it on GitHub.


← Back to Articles