stamatios
← Voltar ao feed
CPU de engrenagens montada com peças empilhadas sobre bancada, tutorial que constrói máquina virtual LC-3 em 250 linhas de C
Dev & Engenharia

Tutorial constrói uma máquina virtual LC-3 completa em cerca de 250 linhas de C

resumo de ~3 min

O projeto

O tutorial de Justin Meiners e Ryan Pendleton ensina a escrever do zero uma máquina virtual capaz de executar programas em assembly, usando cerca de 250 linhas de C. O público-alvo são programadores que sabem o básico de C ou C++ e aritmética binária, mas querem entender o que acontece dentro de um computador. O código final roda jogos como 2048 e um Roguelike, e o texto é um "literate program": o próprio artigo é o código-fonte, com cada bloco explicado.

O que é uma VM e por que ela existe

Uma VM é um programa que simula uma CPU e componentes de hardware, executando uma linguagem de máquina própria. O tutorial distingue dois usos: emuladores, que recriam fielmente um computador específico (como consoles de videogame), e VMs inventadas para dar portabilidade - escreve-se o programa uma vez na linguagem da VM, e a VM é implementada em cada arquitetura. A Java Virtual Machine é citada como exemplo de sucesso: implementada em milhares de dispositivos, permite que qualquer programa Java, Kotlin ou Clojure rode sem modificação, ao custo do overhead da VM. As VMs também servem para isolar execução: o coletor de lixo se torna viável porque a VM enxerga a pilha de fora, e os smart contracts da Ethereum rodam numa VM sem acesso a sistema de arquivos ou rede, já que os operadores de nó executam código escrito por estranhos.

A arquitetura LC-3

A VM simula o LC-3, arquitetura educacional descrita no livro de Patt e Patel, com conjunto de instruções simplificado que demonstra as ideias centrais das CPUs modernas. Os componentes implementados são:

  • Memória: 65.536 posições de 16 bits (128 KB), armazenadas num simples array.
  • Registradores: 10 no total - 8 de uso geral (R0–R7), um contador de programa (PC) e um de flags de condição (COND), com três flags: positivo, zero e negativo.
  • Instruções: 16 opcodes, cada instrução com 16 bits, sendo 4 para o opcode. O contraste com o x86 (centenas de instruções) serve para explicar a diferença entre RISC e CISC.

Execução e implementação das instruções

O ciclo de execução é simples: buscar a instrução no endereço do PC, incrementar o PC, decodificar o opcode, executar e repetir. Saltos do tipo "goto" alteram o fluxo, então o programa não "acaba". O autor observa a ideia filosofalmente notável desse design - qualquer programa LC-3, por mais complexo, roda sobre esse código simples - e cita Alan Turing sobre máquinas que surpreendem.

O tutorial detalha a implementação de duas instruções e fornece o cheat sheet do resto. ADD tem dois modos: registro (o segundo operando vem de outro registrador) e imediato (o valor está embutido nos 5 bits finais da instrução, útil para incrementos, mas limitado a 32 valores). Como os valores imediatos têm menos de 16 bits, é preciso fazer extensão de sinal para preservar números negativos. Toda escrita em registrador atualiza as flags de condição. LDI (load indirect) resolve a limitação de endereçamento de 9 bits: calcula um endereço somando o deslocamento ao PC, lê esse endereço, e o valor encontrado é outro endereço - o endereço final dos dados. O tutorial compara isso a um ponteiro em C.

Rotinas de interrupção (trap routines)

As trap routines são a "API" do LC-3: códigos predefinidos para teclado e console (GETC, OUT, PUTS, IN, PUTSP, HALT), que na máquina real vivem em endereços baixos da memória - por isso os programas começam em 0x3000. Diferente do simulador oficial, que as escreve em assembly, o tutorial as implementa em C, aproveitando as funções de I/O do sistema operacional - por exemplo, esperar por entrada de teclado em vez de gastar CPU verificando continuamente.

Carregamento e detalhes finais

Os programas vêm de arquivos de imagem cujos primeiros 16 bits indicam o endereço de origem; o restante é copiado para a memória, com troca de ordem de bytes (endianness), já que programas LC-3 são big-endian e computadores modernos são little-endian. Dois registradores mapeados em memória, KBSR e KBDR, permitem verificar o estado do teclado sem bloquear a execução, o que exige funções de acesso à memória em vez de leitura direta do array. A seção final cobre detalhes de plataforma (terminal no Unix, APIs do Windows) que podem ser copiados sem estudo.

A conclusão implícita é que escrever uma VM é mais simples do que parece: o núcleo é um laço com um switch de 16 casos, e o conhecimento de uma instrução se reutiliza nas demais.