A área de emulação é muito ampla e complexa. A seguir, serão apresentadas os componentes básicos, apenas. É recomendado que se tenha algum conhecimento de como funcionam os processadores internamente, assim como um tantinho de assembly (que, notem, é diferente de assembler).
Ideia básica
O emulador toma conta do processador e dos componentes do equipamento. Depois, cada componente é conectado, da mesma maneira que é feito com os fios e circuitos físicos.
Emulando o processador
Há três maneiras de se emular um processador:
- Interpretação
- Recompilação dinâmica
- Recompilação estática
Seguindo qualquer um desses caminhos, você terá um pedaço de código que modifica o estado interno do processador e interage com o “hardware”. Estado do processador é um conglomerado de registrador de processador, gerenciador de interrupções, etc, para um determinado processador. Para o 6502, você teria um número de inteiros 8-bit representando registros: A, X, Y, P, e S. Você também teria um registrador de 16-bits PC.
Com interpretação, você começa no PI (Ponteiro de Instrução — também chamado de PC – Contador de programa), e lê a instrução da memória. Seu código varre essa instrução e usa a mesma para alterar o estado do processador como especificado pelo processador. O problema com interpretação é que é muito lenta. A cada instrução dada, tem-se que decodificá-la, e realizar a operação solicitada.
Com recompilação dinâmica, você também intera pelo código assim como na interpretação, mas ao invés de executar você monta uma lista de operações. Uma vez que se atinja uma instrução de bracha, você compila a lista de instruções em código de máquina para a plataforma hospedeira, faz um cache desse código e a executa.
Então quando você manda um determinado grupo de instrução novamente, você tem apenas de executar o código a partir da cache. (por sinal, a maior parte das pessoas não fazem realmente uma lista de instruções, mas sim as compilam em código de máquina “on the fly”. — isso o deixa mais difícil de otimizar, mas está fora do escopo responder a essa pergunta, a não ser que haja interesse suficiente)
Com recompilação estática, você faz o mesmo que na recompilação dinamica, mas você segue as brechas. Você acaba construindo um trecho de código que representa todo o código do programa, o qual pode ser executado sem interferência posterior. Isso seria um grande mecanismo se não fosse pelos seguintes problemas:
- Código que não está no programa que começa com (ex. compressed, encrypted, generated/modified at runtime, etc) não será recompilado, então não irá rodar.
- Foi provado que achar todo o código em um determinado binário é equivalente ao Halting problem.
Esses se combinam para fazer com que a recompilação estática inviável em 99% dos casos. Para mais informações, Michael Steil fez uma ótima pesquisa em recompilação estática — a melhor que eu já vi.
O outro lado da emulação de processador é a maneira pela qual você interage com o hardware. Isso tem dois lados:
- Timing do processador
- Gerenciamento de interrupções
Timing do processador:
Algumas plataformas — especialmente as mais velhas como NES, SNES, etc — requer que seu emulador tenha timing estrito para ser complemtamente compatível. Com o NES, você tem o PPU (pixel processing unit, unidade de processamento de pixel) que requer que a CPU ponha pixels na sua memória em um momento preciso. Se você usa interpretação, você pode contar ciclos facilmente e emular o timing apropariado; com recompilação dinâmica/estática, as coisas são bem mais complicadas.
Gerenciamento de interrupções:
Interrupções são o mecanismo principal que a CPU comunica com o hardware. Geralmente, seus componentes de hardware irão dizer à CPU que interrupção ela cuida.
Isso é bem elementar — quando o seu código gera uma determinada interrupção, você olha na tabela de gerenciador de interrupção e chama o callback apropriado.
Emulação do hardware:
Há dois lados em emular um determinado dispositivo de hardware:
- Emulando as funcionalidade do dispositivo
- Emulando as interfaces do dispositivo atual
Tome o caso do disco rígido. A funcionalidade é emulada ao se criar a armazenagem, rotinas de leitura/escrita/formatação, etc. Essa parte é geralmente elementar.
A interface atual do dispositivo é um pouco mais complexa. Isso é geralmente uma combinação de registros de memória mapeada (ex: partes da memória que o dispositivo observa para mudanças para fazer sinalização) e interrupções. Para um disco rígido, você pode ter uma área mapeada na memória aonde você lê comandos, escreve, etc, então lê os dados novamente.
Eu iria em mais detalhes, mas há um milhão de maneiras que você poderá ir com isso. se você tem quaisquer questões específicas, sinta-se livre para perguntar e eu adicionarei as informações.
Eu acho que fiz uma boa introdução aqui, mas há uma tonelada de áreas adicionais. Eu estou mais do que feliz de ajudar com qualquer pergunta. Se fui vago em alguma parte, é devido à complexidade do assunto.
Post fortemente inspirado na excelente postagem vista no Stack Overflow.