Virtualizando o 6502 sobre o 6502 com o 6o6
(oldvcr.blogspot.com)- 6o6 é um projeto que executa um 6502 novamente em software sobre um NMOS 6502 com poucos recursos de proteção, adicionando uma camada de execução virtual controlável a sistemas antigos de 8 bits
- Intercepta a execução das instruções e os acessos à memória do código convidado para oferecer recursos como remapeamento de endereços, bloqueio de leituras/escritas ilegais e trap para opcodes jam
- O ponto central é reutilizar diretamente a ALU do 6502 hospedeiro: os registradores e flags do convidado são carregados no hospedeiro, a mesma instrução é executada e depois o resultado é salvo de volta
- A validação usou a 6502 functional test suite de Klaus Dormann e um ambiente de testes baseado em lib6502; a configuração otimizada executou 1.602.516.769 instruções, 36,5% menos que a configuração não otimizada
- O The Incredible KIMplement 1.0 e vários exemplos lançados junto mostram o alcance do 6o6, incluindo emulação de KIM-1, virtualização aninhada, troca de tarefas e até um sistema de memória externa baseado em geoRAM
O que o 6o6 e o KIMplement lançaram
- The Incredible KIMplement 1.0 emula o computador de placa única MOS/Commodore KIM-1 6502 de 1 KB e 1 MHz
- Funciona em um Commodore 64 sem expansões
- Suporta o TTY embutido do KIM e também acesso pela porta serial do computador real
- O espaço de endereçamento foi expandido para 16K
- 6o6 significa “6502-on-6502” e é uma CPU virtual NMOS 6502 totalmente em software executada sobre uma CPU 6502
- Controla a execução do código convidado
- Intercepta opcodes não documentados e opcodes jam
- Abstrai todos os acessos à memória
- Suporta remapeamento de endereços, interceptação de leituras/escritas ilegais e execução baseada em memória virtual
- Executar o
hello worldconvidado em um Commodore 64 e em um Apple IIe funciona, assim como a virtualização aninhada de executar o 6o6 novamente dentro do 6o6- O stage 1 roda quase imediatamente
- O stage 2 é mais lento
- O stage 3 é muito lento, mas funciona
Por que o 6502 precisa de virtualização
- Nos primeiros computadores pessoais, normalmente um único programa controlava a máquina inteira, e se algo desse errado bastava reiniciar
- Em ambientes multiusuário ou multitarefa, código defeituoso pode corromper outros espaços de endereçamento, executar instruções perigosas ou monopolizar recursos
- O NMOS 6502 é uma CPU simples com menos de cerca de 4.000 transistores, então seus recursos de proteção são limitados
- Muitos sistemas históricos com NMOS 6502 tinham dificuldade para mover livremente a zero page ou a posição da pilha do processador
- Não havia como remapear endereços de código para posições arbitrárias e executá-los sem fixup
- Não era possível proibir de forma ampla o acesso a posições específicas de memória
- Se um opcode jam não documentado ou
KILfosse executado, o processador podia parar completamente
- Alguns problemas podem ser amenizados por hardware
- Gerar NMI periodicamente permite interromper de fora um processo que tente monopolizar o sistema configurando a flag de interrupção
- Alguns kernels multitarefa para 6502 implementavam troca de tarefas preemptiva dessa forma
- Emuladores in-circuit como o “Trap65” da Eastern House Software podiam trocar opcodes ruins por BRK interceptável, mas eram caros e tinham limitações para manipulação complexa do barramento
Como o 6o6 executa
- Uma abordagem de interpretador simples pode ser prática no 6502
- O 6502 tem poucos registradores
- Há 56 instruções e não tantos modos de endereçamento
- É relativamente fácil acompanhar o estado do processador
- A “virtualização” do 6o6 está em usar a ALU do 6502 hospedeiro para as operações internas do convidado
- Carrega o acumulador e as flags do convidado na CPU hospedeira
- Executa no hospedeiro a mesma instrução que o convidado executaria
- Salva o resultado e as flags e limpa o estado do hospedeiro
- A vantagem é não precisar reimplementar aritmética e tratamento de flags diretamente
- O decimal mode, ou seja, aritmética BCD, funciona naturalmente
- Como um 6502 real faz o cálculo, o resultado é o mesmo de um 6502 real
- O mesmo método também é usado ao ler valores de memória ou transferir registradores, para tratar a flag negativa e a flag zero
- A implementação usa código automodificável, então colocá-la em ROM exige cuidados extras
Estrutura de VM, harness e kernel
- A VM 6o6 atua como um motor, não como um sistema completo
- O ambiente de execução é dividido em três partes
- VM: a CPU virtual independente de hardware que roda sobre um 6502 real
- Harness: a interface para a memória convidada e o hardware gerenciado
- Kernel: o loop de controle que chama a VM e trata exceções e estado do convidado
- O harness fornece uma interface binária por meio de uma jump table padronizada
- Implementa load/store para endereços específicos
- Trata instruction fetch
- Mantém a pilha de hardware e o ponteiro da pilha
- A VM não assume tamanho de página nem a existência de paginação de memória
- O harness pode implementar desde tradução de endereços com soma e shift até memória virtual paginada
- Em vez de expor page fault para fora, o harness pode fazer paging in/out durante load/store
- Exceções de proteção também podem ser geradas pelo harness
- O kernel inicia a execução da VM e interpreta o código de status retornado pela VM
- Trata exceções geradas pelo próprio harness ou pelo 6o6
- Pode inspecionar ou modificar os registradores do convidado e o PC
- Pode tratar algumas rotinas de serviço de forma nativa
- Entre chamadas da VM, a CPU convidada fica “parada”, o que permite capturar estado ou trocar contexto
- A VM não gera diretamente IRQ, NMI nem reset virtuais
- O kernel decide quando esses eventos ocorrem
- BRK é suportado, mas a VM prepara a pilha e retorna uma exceção em vez de saltar para um novo PC
Uso do 6o6 no KIMplement
- O harness do KIMplement virtualiza a pilha padrão do 6502 e os dispositivos de expansão KIM-4
$0000-$17ff: memória de leitura/escrita$1800-$1fff: ROM$2000-$3fff: RAM$4000-$fff7: espaço não mapeado sem escrita$1ff8-$1fffé espelhado para$fff8-$ffffpara os vetores
- No Commodore 64 hospedeiro, os 16K inferiores ficam em
$4000-$7fffe o restante é sintetizado pelo harness - O kernel do KIMplement trata emulação de RRIOT, exibição em LED, serviço TTY, injeção de NMI para stop e Single-Step Switch, e trap de parte do monitor ROM do KIM-1
- Após a execução da VM, o kernel do KIMplement inspeciona o PC do 6o6 para decidir se deve interceptar a rotina atual
- O TTY e algumas outras funções são implementados assim
Otimização de desempenho
- As chamadas entre o 6o6 e o harness podem ser um grande gargalo
- Mesmo uma instrução simples precisa de pelo menos um fetch
- Endereçamento indireto pode gerar ainda mais acessos à memória
- No KIMplement 0.2, parte dos loads de memória foi inline com macros de pré-processador
- Uma rotina de load para endereços virtuais arbitrários e outra otimizada para zero page foram ligadas diretamente à VM
- A velocidade melhorou bastante, mas a VM ficou maior
- Store continua como chamada de sub-rotina porque acontece com menos frequência e é mais complexo
- Nas iterações mais recentes, também foi eliminada a ineficiência da forma como as macros inline acessavam o program counter, melhorando ainda mais o instruction fetch
- No KIMplement 0.3, foi adicionada uma fusão de instruções primitiva chamada “extra helpings”
- Instruções que não tocam memória não precisam retornar imediatamente ao kernel
- Isso vale para instruções imediatas, instruções centradas no acumulador, a maioria das instruções implícitas e branches não tomados
- Se houver load/store, mudança não sequencial de PC ou exceção, a VM para de tentar agrupar instruções
- Extra helpings não torna a VM em si mais rápida
- Em casos como o KIMplement, onde funcionalidades são limitadas conforme a posição do PC, pode até ficar um pouco mais lento
- Em compensação, outras partes do sistema ficam mais rápidas porque o kernel deixa de rodar sem necessidade em instruções que não produzem mudanças observáveis
- Para aplicações que precisam de controle fino do PC, há opção de desativação parcial ou total
Validação e resultados de teste
- A validação usou a functional test suite de Klaus Dormann
- O binário fornecido não faz suposições sobre hardware
- O fim bem-sucedido é sinalizado por um loop infinito em uma posição específica
- Os testes foram configurados para rodar diretamente no shell usando o emulador de CPU lib6502 de Ian Piumarta
- O lib6502 inicialmente falhou por causa de edge cases do decimal mode, mas passou após um patch
- Como o binário fornecido por Klaus ocupa 64K completos, ele não cabia junto com o 6o6 no espaço de endereçamento padrão do 6502
- Foi adicionado ao lib6502 um patch de sistema mínimo com bank switching de 32K
- A área
$7000-$effffoi usada, colocando os 32K iniciais e os 32K finais do binário de teste em bancos diferentes
- O teste foi feito em três configurações
- Sem extra helpings e sem inline fetch macro
- Com inline fetch macro e sem extra helpings
- Com inline fetch macro e com extra helpings
- As três configurações passaram na suíte de Klaus
- Os resultados de contagem de instruções foram os seguintes
- lib6502 sem 6o6: 30.646.178 instruções
- 6o6 sem otimizações: 2.188.322.914 instruções
- Com inline fetch macro: 1.713.350.225 instruções
- Com inline fetch macro e extra helpings: 1.602.516.769 instruções
- A configuração mais rápida do 6o6 executou 36,5% menos instruções que a configuração menos otimizada
- A configuração mais rápida executou em média 52,3 instruções por instrução convidada
- Esse número inclui harness, kernel e execução do 6o6
- Como cada instrução tem cycle count diferente, isso não deve ser interpretado como fator direto de velocidade
Exemplos incluídos
- O exemplo hello world primeiro roda o mesmo programa na CPU nativa e depois via 6o6
- No Commodore 64, ele é mapeado para a rotina de saída de caracteres em
$ffd2; no Apple II, para$fded - Quando detecta que o PC aponta para a rotina de saída de caracteres, o kernel pega o acumulador do convidado, chama a rotina ROM nativa e retira o return address da pilha para voltar ao loop
- No Commodore 64, ele é mapeado para a rotina de saída de caracteres em
- O exemplo inception usa o mesmo harness e kernel para fazer o 6o6 executar a si próprio como payload
- Cada stage tem sua própria zero page e pilha
- Como o 6o6 atual usa código automodificável, cada stage precisa de sua própria cópia da VM
- No stage 3, a maior parte da memória é consumida por três cópias da VM, e a VM com inline fetch macro ocupa mais de 10 KB por cópia
- Na execução aninhada, a chamada
CHROUTno stage 3 passa pelo stage 2 e pelo stage 1 antes de chegar à rotina nativa final - O encerramento do payload usa a instrução RTS como um “kick”
- Como não há return address na pilha no início, o RTS provoca stack underflow
- Se o harness reporta isso como exceção, o kernel trata como encerramento normal
- O mesmo mecanismo se propaga até os kernels superiores em stages mais profundos
- No Apple II, é possível executar de novo com
CALL 2051; no Commodore 64, comRUN- A versão para Apple II usa até a área residente do DOS acima de
$9000, então é recomendável reiniciar depois da execução
- A versão para Apple II usa até a área residente do DOS acima de
Exemplo de troca de tarefas
- O exemplo tasks é um pequeno kernel de task switching que alterna entre duas tarefas independentes
- Cada tarefa tem sua própria zero page, pilha e uma pequena faixa de endereços de código, sem saber da existência uma da outra nem da VM
- Uma tarefa mostra o alfabeto e a outra mostra números
- Os números são exibidos em reverse video para distinção visual
- A cada tecla pressionada, a tarefa ativa é trocada
- As duas tarefas usam a mesma posição da zero page para salvar estado, mas como cada uma tem sua zero page independente, ambas retomam de onde pararam
- Para fazer a troca de contexto, basta manter as informações da tarefa atual e uma área de salvamento para A, X, Y, P, S e PC de cada tarefa
- O harness consulta qual tarefa está “na CPU” para escolher os endereços físicos da zero page, da pilha e do código em execução
- O kernel salva/carrega os demais estados na troca e marca outra tarefa como “no processador”
Exemplo de memória externa de 64K baseada em geoRAM
- O exemplo vmgr é específico para Commodore 64 e fornece um espaço de endereçamento de 64K em memória externa sem usar a RAM do próprio sistema
- A geoRAM é um dispositivo de RAM paginada diferente da REU oficial da Commodore
- A REU é centrada em DMA e usa o MOS 8726 REC para operar leitura, escrita e troca com a memória principal
- A geoRAM mapeia memória por uma janela de 256 bytes na faixa de I/O
$de00 - Os registradores de controle ficam em
$dffee$dfff - Clones compatíveis modernos podem ter até 4 MB de capacidade
- O VICE oferece suporte à emulação de geoRAM
- O exemplo usa a ROM do módulo processador 6502 fornecido para o kit RC2014 Z80
- A ROM contém um monitor e o EhBASIC de Lee Davison
- A ROM usada é uma pre-built ROM do GitHub, na variante 6551
- O harness ignora writes a partir de
$c100, que é a área ROM do convidado- Escritas abaixo de 16K usam caminho rápido
- Acima disso, o banco da geoRAM é ajustado com mask e shift
- A página atual da geoRAM é armazenada em cache para pular reconfiguração em acessos à mesma página
- Neste exemplo, kernel e programa principal foram unidos
- Verificam a presença e o funcionamento da geoRAM
- Copiam a imagem ROM para a geoRAM
- BRK retorna ao monitor
- Illegal instruction, trap de instrução definida pelo usuário e similares são tratados como BRK
- O vetor serial da ROM do RC2014 é interceptado para emular um terminal simples
- Faz conversão entre PETSCII e caracteres de terminal
- Mantém um pequeno cursor
- Ajusta registradores e flags do convidado conforme o resultado
CTRL-SHIFT-Commodorepode resetar o sistema emulado sem perder a memória
- No cold start do EhBASIC, se o tamanho da memória não for informado manualmente, a combinação de C64 com geoRAM leva cerca de 1 minuto para encontrar 32768 bytes livres
- A ROM foi compilada com hard cap em
$8000, então o limite fica em 32768 bytes mesmo que exista mais memória - O intervalo
$8000-$c0ffpode ser usado para outras coisas - O EhBASIC não aceita comandos ou keywords em minúsculas, então tudo precisa ser digitado em maiúsculas
- A ROM foi compilada com hard cap em
- Também funciona em um Commodore 128DCR real com cartridge geoRAM de 512K
- Operações de ponto flutuante funcionam normalmente
- Instruções inválidas são interceptadas imediatamente de forma controlada
- Tirando a janela de 256 bytes, o sistema em tela não está rodando dentro do espaço de endereçamento nativo do próprio 6502
- Mesmo com 512K de geoRAM, é possível manter separadamente oito tarefas de um sistema 6502 de 64K
Melhorias futuras e casos de uso
- É possível melhorar o 6o6 para rodar em ROM, mas isso exigiria refatoração e talvez o deixasse mais lento, então é mais uma opção do que uma prioridade
- Emulação de 65816 está fora de escopo, mas pode ser possível emular instruções CMOS em sistemas NMOS
- Como usa a ALU, se um NMOS 6502 emular um CMOS 65C02, as flags ainda serão ajustadas no estilo NMOS
- O inverso também vale
- O endereçamento hoje está escrito no estilo da CPU NMOS
- A abordagem de inline memory macro ainda tem espaço para peephole optimization
- Poderia existir uma etapa “post-preprocessor” antes do assembly final
- Como isso complicaria a toolchain, ainda seria preciso confirmar o ganho prático geral
- Um dos usos explícitos do 6o6 é executar código baixado sem estragar a tarefa atual
- Há a ideia de usá-lo em parte de um cliente Gopher para executar dinamicamente conteúdo baixado
- Se alguém for projetar um novo sistema 6502 do zero, pode ser mais rápido implementar em hardware os recursos necessários
- Quando se trabalha com uma CPU NMOS com poucos mecanismos de proteção ou se quer minimizar silício adicional, o 6o6 vira uma alternativa flexível e adaptável
Distribuição e licença
- The Incredible KIMplement está disponível na homepage e no GitHub
- O 6o6 está disponível no GitHub, junto com os quatro exemplos abordados no texto
- A atualização KIMplement 1.0 foca principalmente em organização para publicação e pequenas correções de bugs
- O KIMplement também inclui o Tiny PILOT fornecido por Dave Hassler
- Tiny PILOT é a implementação escrita por Nicholas Vrtis para a revista MICRO em 1979, com patches de Bob Applegate e Dave Hassler
- Dave Hassler também portou o ELIZA a partir da implementação Atari PILOT de 1980 de Carol Shaw e Harry Stewart
- Tanto o KIMplement quanto o 6o6 são distribuídos sob a Floodgap Free Software License
1 comentários
Opiniões no Hacker News
Mesmo sendo o 6502 simples e limitado, é sempre interessante ver uma arquitetura de quase 50 anos continuar sendo levada a novos limites
Alguns SoCs voltados ao mercado de baixíssimo custo e produção em massa ainda incluem núcleos 6502
Não parece fácil competir com um núcleo RISC-V de 10 centavos
No começo achei engraçada a ideia de um SoC multicore com 6502 núcleos 6502, mas acho que seria um projeto divertido de fazer em FPGA
Uso no Apple 2 um 65816, sucessor de 16 bits, como placa de expansão de velocidade variável, mas na maior parte do tempo ele roda em modo de 8 bits. É porque funciona bem, e a maior parte do código de biblioteca também é de 8 bits
Nessa velocidade, o chip é rápido. Especialmente se pensarmos no modelo simples em que a RAM e a CPU são clockadas 1:1. No meu caso, consigo executar código por um barramento de 1 MHz, então a maioria das operações de múltiplos ciclos acaba parecendo um único ciclo de barramento por fetch de memória
Ou então há 1 MB de RAM na placa, e essa RAM opera na velocidade da CPU (0,15~16 MHz). Assim, fica rápido o suficiente para rodar programas grandes escritos em linguagens de alto nível a uma velocidade utilizável. Assembly, claro, fica absurdamente rápido
É um ambiente bem divertido para sair hackeando coisas
Dá para executar o GEOS dentro de uma janela do GEOS?
Este artigo ficou bastante tempo sem comentários, então pensei em dar uma olhada mais tarde, mas o ponto central está em como o Commodore 64 emula um sistema totalmente diferente baseado em 6502
“6o6”, ou seja, “6502-on-6502”, é uma CPU NMOS 6502 completa virtualizada em software rodando sobre uma CPU 6502, com controle total da execução do código convidado, incluindo opcodes não documentados e traps de opcodes jam, e abstrai também todos os acessos à memória
Com isso, é possível fazer remapeamento de endereços, interceptar leituras e escritas ilegais e até executar memória virtual completa. Além de passar em todos os testes funcionais, ele chega a virtualizar a si mesmo virtualizando a si mesmo; é um trabalho impressionante de qualquer ponto de vista, não só da perspectiva do 6502
Também me lembrei daquele vídeo sobre o Zilog Z80 ter modo protegido: https://www.youtube.com/watch?v=DLSUAVPKeYk
Lembrei de quando comecei a aprender assembly 6502 por conta própria. Havia um livro chamado “The Visual Computer”, que vinha com um emulador em disquete, e foi uma experiência realmente reveladora
Encontrei o PDF do livro [1], mas não sei se o software que vinha no disquete ainda existe em algum lugar
[1] https://files.commodore.software/reference-material/books/c6...
“Visual 6502” é o nome do simulador moderno do 6502 em nível de portas e transistores: http://visual6502.org/JSSim/index.html
https://archive.fo/2u3Y8