2 pontos por GN⁺ 2024-05-13 | 1 comentários | Compartilhar no WhatsApp
  • 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 world convidado 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 KIL fosse 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-$ffff para os vetores
  • No Commodore 64 hospedeiro, os 16K inferiores ficam em $4000-$7fff e 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-$efff foi 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
  • 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 CHROUT no 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, com RUN
    • 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

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 $dffe e $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-Commodore pode 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-$c0ff pode 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
  • 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

 
GN⁺ 2024-05-13
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

    • Por que será? Provavelmente para economizar em custos de pesquisa e desenvolvimento, mantendo um projeto que já funciona bem
      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
    • Existe uma quantidade enorme de código que pode ser usado para quase qualquer coisa que você queira fazer
      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...

  • https://archive.fo/2u3Y8