- 6o6 es un proyecto que vuelve a ejecutar por software un 6502 sobre un NMOS 6502 con protecciones limitadas, añadiendo una capa de ejecución virtual controlable a sistemas antiguos de 8 bits
- Intercepta la ejecución de instrucciones y el acceso a memoria del código invitado para ofrecer funciones como remapeo de direcciones, bloqueo de lecturas/escrituras ilegales y trampas para opcode jam
- La idea central es reutilizar directamente la ALU del 6502 anfitrión: carga los registros y flags del invitado en el anfitrión, ejecuta la misma instrucción y luego guarda de nuevo el resultado
- Para la validación se usaron la 6502 functional test suite de Klaus Dormann y un entorno de pruebas basado en lib6502; la configuración optimizada ejecutó 1,602,516,769 instrucciones, un 36.5% menos que la configuración no optimizada
- The Incredible KIMplement 1.0 y varios ejemplos publicados junto con él muestran el alcance de 6o6: emulación de KIM-1, virtualización anidada, cambio de tareas e incluso un sistema de memoria externa basado en geoRAM
Lo que publicaron 6o6 y KIMplement
- The Incredible KIMplement 1.0 emula la computadora de placa única KIM-1 6502 de MOS/Commodore, con 1 KB y 1 MHz
- Funciona en un Commodore 64 sin expansiones
- Soporta el TTY integrado del KIM y también permite acceso por el puerto serial de una computadora real
- El espacio de direcciones se amplía a 16K
- 6o6 significa “6502-on-6502” y es una CPU virtual NMOS 6502 completamente por software que corre sobre una CPU 6502
- Controla la ejecución del código invitado
- Atrapa opcode no documentados y opcode jam
- Abstrae todos los accesos a memoria
- Soporta remapeo de direcciones, interceptación de lecturas/escrituras ilegales y ejecución basada en memoria virtual
- También funciona la virtualización anidada, ejecutando un
hello worldinvitado en Commodore 64 y Apple IIe, y luego ejecutando otra vez 6o6 dentro de 6o6- La etapa 1 corre casi de inmediato
- La etapa 2 es más lenta
- La etapa 3 es muy lenta, pero funciona
Por qué hace falta virtualización en 6502
- En las primeras computadoras personales, normalmente un solo programa controlaba toda la máquina, y si algo salía mal se resolvía reiniciando
- En entornos multiusuario o multitarea, el código defectuoso puede dañar otros espacios de direcciones, ejecutar instrucciones peligrosas o acaparar recursos
- El NMOS 6502 es una CPU simple de menos de unas 4,000 compuertas, así que sus funciones de protección son limitadas
- Muchos sistemas históricos basados en NMOS 6502 no podían mover libremente la zero page ni la ubicación del stack del procesador
- No existía una función para remapear direcciones de código a posiciones arbitrarias y ejecutarlas sin fixups
- No se podía prohibir de forma general el acceso a ciertas ubicaciones de memoria
- Si se ejecutaba un opcode jam no documentado o
KIL, el procesador podía detenerse por completo
- Algunos problemas pueden mitigarse con hardware
- Si se genera una NMI periódica, un proceso que intente monopolizar el sistema activando el flag de interrupciones puede ser detenido desde afuera
- Algunos kernels multitarea para 6502 implementaban así cambio de tareas con apropiación
- Emuladores in-circuit como “Trap65” de Eastern House Software podían convertir opcode erróneos en BRK atrapables, aunque eran costosos y tenían límites al manipular el bus
Cómo ejecuta 6o6
- Un enfoque de intérprete simple también puede ser práctico en 6502
- El 6502 tiene pocos registros
- Solo hay 56 instrucciones y no tantos modos de direccionamiento
- Es relativamente fácil seguir el estado del procesador
- La “virtualización” de 6o6 consiste en usar la ALU del 6502 anfitrión para las operaciones internas del invitado
- Carga el acumulador y los flags del invitado en la CPU anfitriona
- Ejecuta en el anfitrión la misma instrucción que debe correr el invitado
- Guarda el resultado y los flags, y limpia el estado del anfitrión
- La ventaja es que no hace falta reimplementar manualmente la aritmética ni el manejo de flags
- El modo decimal, es decir, aritmética BCD, funciona de forma natural
- Como calcula un 6502 real, el resultado coincide con el de un 6502
- Al leer valores de memoria o transferir registros también se usa el mismo método para manejar el flag negativo y el flag zero
- La implementación usa código automodificable, así que si se quiere poner en ROM hay que tomar precauciones adicionales
Estructura de VM, harness y kernel
- La VM de 6o6 actúa como motor, no como sistema completo
- El entorno de ejecución se divide en tres partes
- VM: la CPU virtual independiente del hardware que corre sobre un 6502 real
- Harness: la interfaz con la memoria del invitado y el hardware administrado
- Kernel: el bucle de control que invoca la VM y maneja excepciones y estado del invitado
- El harness ofrece una interfaz binaria mediante una jump table estandarizada
- Implementa load/store para direcciones específicas
- Maneja instruction fetch
- Mantiene el stack de hardware y el stack pointer
- La VM no asume tamaño de página ni la existencia de paginación de memoria
- El harness puede implementar desde transformaciones de direcciones simples con suma y desplazamiento de bits hasta memoria virtual paginada
- En vez de propagar page faults hacia afuera, el harness puede hacer paging in/out durante el proceso de load/store
- También puede generar excepciones de protección
- El kernel inicia la ejecución de la VM e interpreta el código de estado con el que regresa
- Maneja excepciones generadas por el harness o por 6o6 mismo
- Puede inspeccionar o modificar los registros del invitado y el PC
- Puede atender de forma nativa ciertas rutinas de servicio
- Entre llamadas a la VM, la CPU invitada está “detenida”, así que se puede capturar estado o hacer cambio de contexto
- La VM no genera directamente IRQ, NMI ni reset virtuales
- El kernel decide cuándo ocurren esos eventos
- BRK está soportado, pero la VM no configura el stack y salta a un nuevo PC, sino que devuelve una excepción
Uso de 6o6 en KIMplement
- El harness de KIMplement virtualiza el stack estándar de 6502 y el dispositivo de expansión KIM-4
$0000-$17ff: memoria de lectura/escritura$1800-$1fff: ROM$2000-$3fff: RAM$4000-$fff7: espacio no mapeado y no escribible$1ff8-$1fffse refleja en$fff8-$ffffpara los vectores
- El Commodore 64 anfitrión guarda los 16K bajos en
$4000-$7fffy el resto lo sintetiza el harness - El kernel de KIMplement maneja la emulación de RRIOT, la visualización LED, el servicio TTY, la inserción de NMI para stop y Single-Step Switch, y algunas trampas del monitor ROM de KIM-1
- Tras ejecutar la VM, el kernel de KIMplement inspecciona el PC de 6o6 para decidir si intercepta la rutina actual
- El TTY y algunas otras funciones se implementan así
Optimización de rendimiento
- Las llamadas entre 6o6 y el harness pueden convertirse en un cuello de botella importante
- Incluso una instrucción simple requiere al menos un fetch
- El direccionamiento indirecto puede provocar más accesos a memoria
- En KIMplement 0.2 se inlinaron algunas cargas de memoria mediante macros del preprocesador
- Se conectaron directamente a la VM una rutina de carga para direcciones virtuales arbitrarias y otra optimizada para zero page
- La velocidad mejoró bastante, pero la VM creció de tamaño
- Los store se dejaron como llamadas a subrutina porque son menos frecuentes y más complejos
- En iteraciones más recientes también se corrigió la ineficiencia en cómo las macros inline accedían al program counter, mejorando más el instruction fetch
- En KIMplement 0.3 se añadió una forma primitiva de instruction fusion llamada “extra helpings”
- Las instrucciones que no tocan memoria no necesitan volver de inmediato al kernel
- Esto aplica a instrucciones inmediatas, centradas en el acumulador, la mayoría de las implied y ramas no tomadas
- Si ocurre un load/store, un cambio no secuencial del PC o una excepción, la VM deja de intentar agrupar instrucciones
- extra helpings no hace que la VM misma sea más rápida
- Si, como en KIMplement, las funciones se limitan según la posición del PC, incluso puede ser un poco más lento
- En cambio, acelera otras partes del sistema al evitar que el kernel se ejecute innecesariamente por cada instrucción sin cambios observables
- Puede estorbar a aplicaciones que necesitan control fino del PC, así que hay opciones para desactivarlo parcial o totalmente
Validación y resultados de pruebas
- Para validar se usó la functional test suite de Klaus Dormann
- El binario provisto no asume ningún hardware específico
- La finalización exitosa se señala con un bucle infinito en una ubicación determinada
- Las pruebas se prepararon para ejecutarse directamente desde el shell usando el emulador de CPU lib6502 de Ian Piumarta
- Al principio lib6502 falló por casos límite del modo decimal, y pasó después de aplicar un parche
- El binario de Klaus ocupa los 64K completos, así que no se podía alojar junto con 6o6 dentro del espacio de direcciones base del 6502
- Se añadió a lib6502 un parche de sistema mínimo con bank switching de 32K
- Se usó el rango
$7000-$efffy se colocaron los primeros 32K y los segundos 32K del binario de prueba en bancos distintos
- Se probó en tres configuraciones
- Sin extra helpings ni macro inline de fetch
- Con macro inline de fetch, sin extra helpings
- Con macro inline de fetch y extra helpings
- Las tres configuraciones pasaron la suite de Klaus
- Los resultados de conteo de instrucciones fueron los siguientes
- lib6502 sin 6o6: 30,646,178 instrucciones
- 6o6 sin optimizaciones: 2,188,322,914 instrucciones
- Con macro inline de fetch: 1,713,350,225 instrucciones
- Con macro inline de fetch y extra helpings: 1,602,516,769 instrucciones
- La configuración más rápida de 6o6 ejecutó 36.5% menos instrucciones que la configuración menos optimizada
- La configuración más rápida ejecutó en promedio 52.3 instrucciones por cada instrucción del invitado
- Esa cifra incluye harness, kernel y ejecución de 6o6
- Como cada instrucción puede tener distinto conteo de ciclos, no debe interpretarse como un factor directo de velocidad
Ejemplos incluidos
- El ejemplo de hello world ejecuta primero el mismo programa en la CPU nativa y luego a través de 6o6
- En Commodore 64 se mapea a la rutina de salida de caracteres en
$ffd2, y en Apple II a$fded - Cuando el kernel detecta que el PC apunta a la rutina de salida de caracteres, toma el acumulador del invitado, llama a la rutina ROM nativa, saca la return address del stack y vuelve al bucle
- En Commodore 64 se mapea a la rutina de salida de caracteres en
- El ejemplo inception ejecuta 6o6 sobre sí mismo como payload usando el mismo harness y kernel
- Cada etapa tiene su propia zero page y stack
- Como el 6o6 actual usa código automodificable, hace falta una copia separada de la VM para cada etapa
- En la etapa 3, casi toda la memoria se usa en tres copias de la VM; la VM con macro inline de fetch ocupa más de 10 KB por copia
- En la ejecución anidada, la llamada a
CHROUTen la etapa 3 pasa por la etapa 2 y la etapa 1 antes de llegar finalmente a la rutina nativa - La finalización del payload usa la instrucción RTS como una especie de “kick”
- Como al iniciar no hay return address en el stack, RTS provoca un stack underflow
- Cuando el harness lo reporta como excepción, el kernel lo trata como finalización normal
- En etapas más profundas se propaga del mismo modo hasta el kernel superior
- En Apple II se puede volver a ejecutar con
CALL 2051, y en Commodore 64 conRUN- La versión para Apple II también usa el área residente de DOS por encima de
$9000, así que se recomienda reiniciar después de ejecutarla
- La versión para Apple II también usa el área residente de DOS por encima de
Ejemplo de cambio de tareas
- El ejemplo tasks es un pequeño kernel de cambio de tareas que alterna entre dos tareas independientes
- Cada tarea tiene su propia zero page, stack y una pequeña región de direcciones de código, y no sabe de la otra ni de la existencia de la VM
- Una tarea muestra el alfabeto y la otra muestra números
- Los números se muestran en reverse video para distinguirlos visualmente
- Cada vez que se presiona una tecla, se cambia de tarea
- Ambas tareas usan la misma ubicación de la zero page para guardar estado, pero como la zero page es independiente, cada una continúa donde se quedó
- Para el cambio de contexto solo hace falta la información de la tarea actual y un área para guardar A, X, Y, P, S y PC de cada tarea
- El harness observa qué tarea está “on CPU” y selecciona la dirección física de la zero page, el stack y el código ejecutable
- Al cambiar, el kernel guarda/carga el resto del estado y marca otra tarea como “on processor”
Ejemplo de memoria externa de 64K basada en geoRAM
- El ejemplo vmgr es exclusivo para Commodore 64 y ofrece como memoria externa un espacio de direcciones de 64K que no usa la RAM del sistema
- geoRAM es un dispositivo de RAM paginada distinto a la REU oficial de Commodore
- La REU se centra en DMA y usa el MOS 8726 REC para hacer operaciones de lectura, escritura e intercambio con la memoria principal
- geoRAM mapea memoria con páginas ventana de 256 bytes en el rango de E/S
$de00 - Sus registros de control están en
$dffe,$dfff - Los clones compatibles modernos pueden llegar a 4 MB de capacidad
- VICE soporta emulación de geoRAM
- El ejemplo usa la ROM del módulo de procesador 6502 ofrecido para la computadora modular RC2014 con kit Z80
- La ROM incluye monitor y EhBASIC de Lee Davison
- La ROM usada es una pre-built ROM de GitHub, en su versión 6551
- El harness ignora las escrituras hacia la región de ROM invitada desde
$c100- Para escrituras por debajo de 16K usa una ruta rápida
- Por encima de eso ajusta el banco de geoRAM con mask y shift
- Mantiene en caché la página actual de geoRAM para omitir configuración cuando se accede a la misma página
- En este ejemplo, el kernel y el programa principal están fusionados
- Verifica la presencia y el funcionamiento de geoRAM
- Copia la imagen ROM a geoRAM
- BRK devuelve al monitor
- Illegal instruction, user-defined instruction trap y otros casos se tratan como BRK
- Intercepta el vector serial de la ROM de RC2014 para emular un terminal simple
- Convierte entre PETSCII y caracteres de terminal
- Mantiene un cursor pequeño
- Ajusta los registros y flags del invitado de acuerdo con el resultado
- Con
CTRL-SHIFT-Commodorese puede resetear el sistema emulado manteniendo la memoria
- Si en el arranque en frío de EhBASIC no se introduce manualmente el tamaño de memoria, encontrar 32768 bytes libres en la combinación de C64 y geoRAM toma cerca de un minuto
- La ROM fue compilada con hard cap en
$8000, así que aunque haya más memoria, queda limitada a 32768 bytes - En
$8000-$c0ffse puede colocar otra cosa - EhBASIC no acepta comandos ni palabras clave en minúsculas, así que todo debe ingresarse en mayúsculas
- La ROM fue compilada con hard cap en
- También funciona en un Commodore 128DCR real con un cartucho geoRAM de 512K
- Las operaciones de punto flotante funcionan bien
- Las bad instruction se interceptan inmediatamente de forma controlada
- Salvo la ventana de 256 bytes, el sistema en pantalla no corre en el espacio de direcciones propio del 6502
- Incluso con 512K de geoRAM se pueden alojar por separado ocho sistemas 6502 de 64K como task 8
Mejoras futuras y posibles usos
- Es posible mejorar 6o6 para que pueda correr desde ROM, pero requeriría refactorización y quizá sería más lento, así que se ve más como una opción
- La emulación de 65816 queda fuera de alcance, aunque podría ser posible emular instrucciones CMOS en sistemas NMOS
- Como usa la ALU, si un NMOS 6502 emula un CMOS 65C02, los flags seguirán fijándose al estilo NMOS
- Lo mismo ocurre en sentido contrario
- El direccionamiento está escrito actualmente según el comportamiento de una CPU NMOS
- El enfoque de macros inline de memoria tiene oportunidades para peephole optimization
- Podría agregarse un paso “post-preprocessor” antes del assembly real
- Como eso complica más la toolchain, primero habría que comprobar si el beneficio general vale la pena
- Uno de los usos explícitos de 6o6 es ejecutar código descargado sin arruinar la tarea actual
- Existe la idea de usarlo como parte de un cliente Gopher para ejecutar dinámicamente lo descargado
- Si alguien diseñara directamente un sistema 6502 nuevo, implementar por hardware las funciones necesarias probablemente sería más rápido
- Si se trabaja con una CPU NMOS con pocas protecciones o se quiere minimizar silicio adicional, 6o6 ofrece una alternativa flexible y adaptable
Distribución y licencia
- The Incredible KIMplement está disponible en su sitio web y en GitHub
- 6o6 está disponible en GitHub e incluye los cuatro ejemplos mencionados en el artículo
- La actualización de KIMplement 1.0 se centra en ordenar el proyecto para publicación y corregir pequeños bugs
- KIMplement también incluye Tiny PILOT, aportado por Dave Hassler
- Tiny PILOT es una implementación escrita por Nicholas Vrtis para la revista MICRO en 1979, con parches añadidos por Bob Applegate y Dave Hassler
- Dave Hassler también porteó ELIZA desde la implementación Atari PILOT de 1980 de Carol Shaw y Harry Stewart
- Tanto KIMplement como 6o6 se distribuyen bajo la Floodgap Free Software License
1 comentarios
Comentarios en Hacker News
Incluso siendo el 6502 algo simple y limitado, siempre resulta interesante ver cómo una arquitectura de casi 50 años sigue siendo llevada a nuevos límites
Algunos SoC orientados a mercados de ultra bajo costo y producción masiva todavía incluyen un núcleo 6502
No parece fácil superar a un núcleo RISC-V de 10 centavos
Al principio me hizo gracia imaginar un SoC multinúcleo con 6502 núcleos 6502, pero hecho en FPGA parece que sería un proyecto divertido
Estoy usando en un Apple 2 una tarjeta de expansión de velocidad variable con el sucesor de 16 bits, el 65816, pero la mayor parte del tiempo corre en modo de 8 bits. Es porque funciona bien y la mayor parte del código de bibliotecas también es de 8 bits
A esta velocidad, el chip es rápido. Más todavía si piensas en un modelo simple donde la RAM y la CPU van sincronizadas 1:1 por reloj. En mi caso puedo ejecutar código a través de un bus de 1 MHz, así que la mayoría de las operaciones de múltiples ciclos terminan siendo como un solo ciclo de bus por fetch de memoria
O bien la tarjeta tiene 1 MB de RAM y esa RAM funciona a la velocidad de la CPU (0.15~16MHz). Eso la hace lo bastante rápida como para que programas grandes escritos en lenguajes de alto nivel corran a una velocidad aceptable. Obviamente, en ensamblador es ridículamente rápida
Es un entorno bastante divertido para ponerse a hackear distintas cosas
¿Se puede ejecutar GEOS dentro de una ventana de GEOS?
Este post estuvo un buen rato sin comentarios, así que pensé en revisarlo después, pero la clave está en cómo el Commodore 64 emula un sistema completamente distinto basado en 6502
“6o6”, o “6502-on-6502”, es un CPU NMOS 6502 totalmente virtualizado por software que corre sobre un CPU 6502, e incluye control total de la ejecución del código huésped, incluso con opcodes no documentados y trampas para opcodes jam, además de abstraer por completo todos los accesos a memoria
Así que permite remapeo de direcciones, interceptar lecturas y escrituras ilegales, e incluso ejecución completa con memoria virtual. No solo pasa la prueba funcional completa, sino que también virtualiza una copia de sí mismo que a su vez se virtualiza a sí misma, así que es un trabajo impresionante no solo desde la perspectiva del 6502, sino desde cualquier perspectiva
También me hizo pensar en un video sobre cómo el Zilog Z80 tiene modo protegido: https://www.youtube.com/watch?v=DLSUAVPKeYk
Me hizo recordar cuando aprendía ensamblador 6502 por mi cuenta. Había un libro llamado “The Visual Computer” y venía con un emulador en un disquete; de verdad fue una experiencia reveladora
Encontré el PDF del libro [1], pero no sé si el software que venía en el disquete todavía existe en algún lado
[1] https://files.commodore.software/reference-material/books/c6...
“Visual 6502” es el nombre de un simulador moderno del 6502 a nivel de compuertas y transistores: http://visual6502.org/JSSim/index.html
https://archive.fo/2u3Y8