quarta-feira, 18 de julho de 2007

Como escrever um emulador de computador por Marat Fayzullin

[Tradução livre de http://fms.komkon.org/EMUL8/HOWTO.html]
Como escrever um emulador de computador

por Marat Fayzullin

Distribuição não autorizada, proibida. Crie um link para esta página, não copie a página.
Eu escrevi este documento depois de receber um monte de email de pessoas que gostariam de escrever um emulador de um ou outro computador (ou console) mas não sabiam por onde começar. Qualquer opinião e avisos contidos no texto abaixo são somente meus and não devem ser tomados como uma verdade absoluta. O documento mostra principalmente os então chamados emuladores "enterpretativos", oposto aos "compilativos", porque eu não tenho muita experiência com tecnicas de recompilação. No documento tem um ou dois links para lugares onde você pode encontrar informações dessas tecnicas.
Se você acha que este documento está faltando algo ou quer fazer uma correção, sinta-se a vontade para enviar um email a mim com seus comentários. Eu não responderei a provocações, idiotices e requisições por ROMS. Eu terrivelmente perdi alguns links (FTP/WWW) importantes na lista de recursos neste documento então se você sabe qualquer um para ser adcionado aqui, diga me sobre eles. O mesmo vale para as FAQ (Questões Respondidas Freqüentemente) que não estão neste documento. Este documento foi traduzido para japônes por Bero. Há também uma tradução em Chines, cortesia de Jean-Yun Chen, uma tradução em francês feita por Maxime Vernier. Uma antiga tradução em francês por Guillaume Tuloup pode ou não estar disponivel neste momento. Tradução em espanhol do HOWTO feita por Santiago Romero. Tradução italiana por Mauro Villani. Tradução livre português do brasil por Dreampeppers99.

Conteúdo

Então, você decidiu escrever um emulador? Muito bem, então este documento pode ser útil a você.
Ele mostra um pouco de informações técnicas e perguntas que pessoas perguntam sobre desenvolver emuladores. Também prove como "blueprints" (algo como documentação de ajuda padrão ou manual) para entender os emuladores indo por degraus.

Principal

* O que pode ser emulado?
* O que é emular e como isso difere da simulação?
* É legal emular hardware proprietário?
* O que é emulador interpretativo e como ele difere do emulador compilativo?
* Eu quero escrever um emulador. Onde eu poderia começar?
* Que linguagem de programação posso usar?
* Onde eu consigo informação sobre o hardware que desejo emular?

Implementação

* Como eu emulo a CPU?
* Como eu trato os acessos a memória emulada?
* Ciclos de processamento: o que são eles?

Técnicas de programação

* Como eu otimizo código C?
* O que é low/high-endiness?
* Como fazer um emulador portátil?
* Porque eu devo fazer um programa modular?

# Mais para ser escrito aqui
O que pode ser emulado?
Basicamente, qualquer coisa que tem um micro-processaodor dentro. Claro, somente dispositivos que rodam mais ou menos são interessantes para emular. Estes incluem.

* Computadores
* Calculadoras
* Videogames
* Máquinas tipo Arcade
* etc.

É necessário notar que você pode emular qualquer sistema computacional, mesmo se ele é muito complexo (tais como Commodore Amiga, por exemplo). A performance de tais emulações pode ser bem lenta.
O que é emulação e como difere de simulação?
Emulação é tentar imitar o projeto interno de um dispositivo. Simulação é tentar imitar as funções de um dispositivo. Por exemplo, um programa imitando o hardwar da máquina arcade pacman e uma rom real rodando isto é emulador. Um jogo Pacman escrito por você no seu computador usando os recursos dos seu computador para ser similar ao da máquina arcade é um simulador.
É legal emular hardware proprietário?
Sobre isto pode se dizer que é uma área cinza, aparentemente é legal emular hardware proprietário, desde que as informações não sejam obtidas pro meios ilegais. Você também deve se atentar que é ilegal distribuir ROM (Bios, etc.) junto com o emulador, se a ROM tiver direitos reservados.

O que é um emulador de interpretação e como ele se difere do emulador de recompilanção dinâmica?
Há três esquemas básicos que podem ser usados para um emulador. Eles podem ser combinados para um melhor resultado.

* Interpretação
Um emulador lê código da memory emulada byte a byte, decodifica, e executa os comandos apropriados nos registros emulados, memoria, e I/O (E/S). O algoritmo geral para tais tipos de emulador é:

while(CPUIsRunning)
{
Fetch OpCode
Interpret OpCode
}


Virtudes deste modelo incluem facilidade de "debugar", portatibilidade, e facilidade de sincronização (você pode simplesmente criar um contador de ciclos que passaram e ajustar o resto da sua emulação para esse contador de ciclos)
Uma única, grande e óbvia fraqueza é a baixa performance. A interpretação leva um pouco do tempo de processamento da CPU (UCP) e você pode necessitar um computador muito rápido para rodar seu código a uma velocidade decente.

* Recompilação estática
Nesta técnica, você leva um programa escrito no código emulado e terá que "traduzir" para o código assembly do seu computador. O resultado será um arquivo normal que você poderá rodar no seu computador sem nenhuma especial ferramenta. Enquando recompilação estática parace muito bom, ela nem sempre é possível. Por exemplo, você não pode recompilar estaticamente um código auto-modificáveis como não há um modo de "dizer" que isto será feito sem rodar o programa. Para anular tais situações, você pode tentar combinar compilação estática com um interpretador ou um recompilador dinâmica.

* Recompilação dinamica (dynarec)
Recompilação dinamica é essênciamente a mesma coisa que a estática, porém ela ocore durante a execução do programa. Ao invés de tentar recompilar todo código de uma vez, faz isso em tempo de execução quando você encontra alguma instrução de CPU seja uma chamada (CALL) ou um desvio (JUMP JMP). Para aumentar a velocidadde, esta técnica pode ser combinada com a recompilação estática. Você pode ler mais sobre recompilação dinamica no white paper (como um "artigo manual") por Ardi, criadores do recompilador do emulador de Macintosh.

Eu quero escrever um emulador. Onde eu deveria começar?
Para escrever um emulador, você precisa ter um bom conhecimento geral sobre programação de computadores e aparelhos eletronicos digitais. Experiencia em programação assembly também é muito bem vinda.

1. Selecione uma linguagem de programação.
2. Encontre toda informação disponivel sobre o hardware que deseja emular.
3. Escreva o código da CPU ou pegue um código já existente.
4. Escreva algums códigos para emular o resto do hardware, no minímo parcialmente.
5. Neste ponto, é de grande utilidade escrever um pequeno debugador que permita parar a emulação e ver o que o progrma está fazendo. Você pode também precisar "disassemblar" (decompilar o assembly) do emulador em questão. Escreva seu próprio senão existir.
6. Tente rodar programas no seu emulador.
7. Use o disassembler e o debugador para ver como os programas usam o hardware e ajuste o seu código apropriadamente.

Que linguagem de programação deveria usar?
A mais obvia alternativa é C e Assembly. Aqui estão os prós e contras de cada uma delas:


* Linguagens Assembly

+ Normalmente, permite produzir código mais rápido.
+ A emulação dos registradores da CPU pode ser usada diretamente guardando nos registradores da CPU emulada.
+ Muitas instruções (opcodes) podem ser emulados com similar instruções (opcodes) da CPU emulada.
- O código não é portátil, ex: não pode rodar no computador com uma arquitetura diferente.
- Isto é difícil de debugar e manter o código.

* C

+ O código pode ser portavel e então funcionar em
computadores diferentes e diferetnes sistemas operacionais.
+ É relativamente fácil debugar e manter o código.
+ Diferentes hipoteses de como o hardware real funciona
pode ser testado rapidamente.
- C é normalmente mais lento do que assembly puro.

Bons conhecimentos da linguagem escolhida é absolutamente necessário para escrever um emulador funcional, como isto é um tanto quanto complexo, e seu código deverá ser otimizado para rodar tão rápido quanto possivel. Emular computadores não é defintivamente um projeto onde você aprende uma linguagem de programação.
Onde eu consigo informação do hardware que desejo emular?
Segue a lista de lugares onde você pode querer dar uma olhada.

Newsgroups

* comp.emulators.misc
Este é um newsgroup para discuções gerais sobre emulação. Muitos desenvolvedores de emulador participam, porém o nivel de ruído (especulação) é alto. Leia o c.e.m FAQ antes de postar para este newsgroup.
* comp.emulators.game-consoles
Como o comp.emulators.misc, mas especificamente falando com emuladores de videogame (console). Leia o c.e.m FAQ antes de postar para este newsgroup.
* comp.sys./emulated-system/
O comp.sys.* contém newsgroups dedicado para especificos computadores. Você pode obter um monte de informação técnica lendo este newsgroups.Tipicos exemplos:
comp.sys.msx MSX/MSX2/MSX2+/TurboR computers
comp.sys.sinclair Sinclair ZX80/ZX81/ZXSpectrum/QL
comp.sys.apple2 Apple ][
etc.

Por favor, cheque o apropriado FAQs antes de postar nestes newsgroups.
* alt.folklore.computers
* rec.games.video.classic

FTP
[#] Console and Game Programming site in Oulu, Finland
[#] Arcade Videogame Hardware archive at ftp.spies.com
[#] Computer History and Emulation archive at KOMKON

WWW
[#] My Homepage
[#] Arcade Emulation Programming Repository
[#] Emulation Programmer's Resource

Como eu emulo a CPU (UCP - processador)?
Primeiro de tudo, se você somente precisa emular processadores padrão como Z80 ou 6502, você pode usar um dos emuladores que eu escrevi. Certas condições devem ser observadas para o uso destes processadores.
Para aqueles que querem escrever sua própria CPU ou está interessado em saber como funciona isto, Eu projetei um esqueleto de um tipico CPU em C, abaixo. No emulador real, você pode querer pular algumas partes e adcionar outras na sua própria CPU.

Counter=InterruptPeriod;
PC=InitialPC;

for(;;)
{
OpCode=Memory[PC++];
Counter-=Cycles[OpCode];

switch(OpCode)
{
case OpCode1:
case OpCode2:
...
}

if(Counter<=0)
{ /* Check for interrupts and do other */
/* cyclic tasks here */
...S Counter+=InterruptPeriod;
if(ExitRequired) break;
}
}

Primeiro, nos atribuimos valores iniciais para o contador de ciclos (mhz) do CPU (Counter), e para o contador do programa (PC - Program Counter): Counter=InterruptPeriod; PC=InitialPC; O contador contém o número de ciclos de CPU (ou de processamento como achar melhor) para a próxima interrupção suspeita. Note que a interrupção poderá não necessariamente ocorrer quando este contador expirar: você pode usar isto para muitos outros propositos, tais como sincronização de temporarizadores (timers mais simplesmente), ou atualizar scanlines (algo como a varedura da imagem formada) na tela. Mais tarde falaremos sobre isto. O PC contém o endereço de memória do qual nosso CPU emulado irá ler o próximo opcode (instrução). Depois da "inicialização" dos valores, nos começaremos o laço principal: for(;;) { Note que este laço pode ser implementado como while(CPUIsRunning) { Onde CPUIsRunning é uma variavle booleana. Esta tem certas vantagens, como você pode terminar o loop em qualquer momento apenas atribuindo CPUIsRunning=0. Por outro lado, checar esta variavel toda passagem leva um pouquinho de tempo da CPU, e deveria ser evitado se possivel. Também, não implemente este laço como while(1) { porque neste caso, alguns compiladores irão gerar código para checar se o 1 é true ou não. Você certamente não quer que o compilador faça isso desnecessariamente a cada passo do laço. Agora, quando nos estamos no laço, a primeira coisa é ler o próximo opcode(instrução), e modificar o contador do programa: OpCode=Memory[PC++]; Note que enquanto isto é o jeito mais simples e fácil para ler da memória emulada, isto nem sempre é viavel. Um modo mais universal de acessar a memoria é explicado neste documento. Depois que o opcode (instrução) é recuperada, nos decrescemos o contador de ciclos de CPU pelo número requerido para esta opcode(instrução): Counter-=Cycles[OpCode]; O vetor Cycles[] deverá conter o número de ciclos de CPU para cada opcode(instrução). Esteja atendo para que alguns opcodes (tais como desvios condicionais or chamada de subrotinas (JMP ou CALL)) podem ter differentes números de ciclos dependendo dos argumentos. Isto pode ser ajustado mais tarde no código. Agora vem a hora de interpretar o opcode(instrução) e executar-la: switch(OpCode) { É um conceito comum que o comando switch() é ineficiente, como ele na verdade compila dentro de uma cadeia de comandos if() (se)... Enquando isto é verdade para construtores com pequenas opções, grandes construtores (de 100 a 200 ou mais opções) sempre aparecem para compilar dentro de uma uma tabela de desvios, que faz isto mais eficiente. Há duas alternativas para interpretar os opcodes. A primeira é fazer uma tabela de funções e chamar a apropriada. Este método parece ser menos eficiente do que switch(), como você tem o overhead (execesso) das chamadas de funções. O segundo método deveria ser feito uma tabela de etiquetas e usar os comandos goto. Enquanto este método é ligeramente mais rápido do que o comando switch(), isto provavelmente só irá funcionar em compiladores que aceitem "etiquetas (labels) precomputados". Outros compiladores não irão permitir você criar uma matriz de endereços de etiquetas. (label). Depois de ter interpretado e executado o opcode com sucesso, é hora de checar o estado atual do CPU nos precisamos de alguns interruptores. Neste momento, você também pode executar qualquer tarefa que precise ser sincronizada com o clock do sistema: if(Counter<=0) { /* Cheque por interruptores e outros hardwares da emulação aqui */ ... Counter+=InterruptPeriod; if(ExitRequired) break; } Estas tarefas de ciclos de CPU são explicadas mais tarde neste documento. Note que nos não simplesmente atribuimos Counter=InterruptPeriod, mas foi feito um Counter+=InterruptPeriod: isto faz o contador de ciclos mais preciso, como isto pode ser um número negativo a este contador Counter. Também, de uma olhada if(ExitRequired) break; linha. Como é tão custoso checar por uma saida toda passagem do laço, nos somente faremos isto quando o Counter expirar: isto irá dar uma saida da emulação quando você atribuir ExitRequired=1, mas isto não gastará tanto tempo de CPU.

Como controlar o acesso a memória emulada?

O mais simples jeito para acessar a memória emulada é tratar-la como um vetor de bytes(palavras(words), etc).
O acesso se torna trivial: Data=Memory[Address1]; /* Ler de Address1 */ Memory[Address2]=Data; /* Escrever no Address2 */ Tal modo de acessar a memória não é sempre possivel pelas seguintes razões: * Memória páginada O espaço de endereço pode ser fragmentado dentro de páginas de trocas (swaps) (também conhecidas como banks (ou bancos)). Isto é feito para expandir a memória quando o endereçamento de memória é pequeno (64Kb). * Memória espelhada Uma área da memória pode ser acessível em muitos endereços diferentes. Por exemplo, os dados que você escreve na locaziação $4000 (em hexadecimal) também aparecerá em $6000 e $8000. O ROM pode também ser espelhado devido a um endereço incompleto. * Proteção da ROM Alguns softwares baseados em cartuchos (como MSX, por exemplo) tentam escrever dentro da sua própria ROM e recusam funcionar se a escrita for bem sucedida. Isto é feito para proteção de cópia. Fazer tais software trabalharem no seu emulador, você deverá desabilitar escritas dentro da ROM. * Memória mapeada I/O (E/S) Poderá existir dispositivos baseados em memória mapeadas no sistema. Acessando locais na memória que produzem "efeitos especias" e eles deverão ser tratados para tal. Para solucionar estes problemas, nos introduziremos um par de funções: Data=ReadMemory(Address1); /* Ler do Address1 */ WriteMemory(Address2,Data); /* Escrever em Address2 */ Todo processamento especial tal como acesso a página, espelhamento, controle I/O, etc., é feito dentro dessas funções. ReadMemory() e WriteMemory() normalmente coloca um pouco de overhead (excesso) na emulação porque elas são chamadas freqüêntemente. Conseqüêntemente, elas precisam ser feitas o mais eficiente possivel. Aqui um exemplo disto essas funções escrevem o acesso a endereço páginado: static

inline byte ReadMemory(register word Address) { return(MemoryPage[Address>>13][Address&0x1FFF]);
}

static inline void WriteMemory(register word Address,register byte Value)
{
MemoryPage[Address>>13][Address&0x1FFF]=Value;
}

Aviso sobre a palavra-chave inline. Ela irá dizer ao compilador para embutir a função dentro do código, ao invés de fazer chamadas para ele. Se seu compilador não suporta inline ou _inline, tente fazer funções estáticas alguns compiladores (WatcomC, por exemplo) irá optimizar o static para inline.

Também, tenha em mente que na maioria dos caos o ReadMemory() é chamado muitas vezes mais freqüente do que WriteMemory(). Conseqüêntemente, será preciso implemtar a maoria do código em WriteMemory() deixando o ReadMemory() tão pequeno e simple quanto for possivel.



* Uma pequena observação no espelhamento da memória:
Como foi dito anteriormente, muitos computadores tem a memória RAM espelhada onde o valor escrito dentro de uma localização irá aparecer em outras. Enquanto esta situação pode ser controlada na ReadMemory(), normalmente não é desejavel, já que ReadMemory() é chamada muito mais frequentemente do que WriteMemory().Um jeito mais eficiente poderia ser implementar o espelhamento na função WriteMemory().

Tarefas de ciclos: o que são elas?
Tarefas de ciclos são coisas que deveriam ocorrer periodicamente numa máquina emulada, tais como:

* Atualização da tela
* Interruptores VBlank e HBlank
* Atualizar os temporizadores (timers)
* Atualizar os parâmetros de som
* Atualizar o estado do Teclado/joysticks
* etc.

Para emular tais tarefas, você deverá amarar-las ao apropriado número de ciclos de CPU. Por exemplo, se a CPU roda a 2.5MHz (a 2 milhões e 500 mil ciclos de CPU) e um display (parte ou todo do monitor que mostra a tela emulada) usa 50Hz (50 Ciclos de CPU) para atualizar a freqüência (padrão para video do tipo PAL), o interruptor VBlank irá ocorrer cada

2500000/50 = 50000 Ciclos de CPU

Agora, se assumimos que a tela intera (incluindo VBlack) é de 256x212 scanlines (44 caem para o VBlank), nos temos que sua emulação precisa atualizar a scanline a cada

50000/256 ~= 195 Ciclos de CPU

Depois disto, você deverá gerar um VBlank e então não fazer nada até que tenha passado o tempo para

o VBlank

(256-212)*50000/256 = 44*50000/256 ~= 8594 Ciclos de CPU

Cuidadosamente calculamos o número de ciclos necessários para cada tarefa, então usamos o maior divisor comum para o InterruptPeriod e vinculamos todas outras tarefas para ele. (elas não necessáriamente executaram a cada expiração do Counter).

Como eu otimizo código C?
Primeiro, um monte de código performático pode ser atingido pela escolha correta das opções para otimização do próprio compilador. Baseado em minha expêriencia, segue abaixo a combinação de flags que irão dar a melhor velocidade de execução:

Watcom C++ -oneatx -zp4 -5r -fp3
GNU C++ -O3 -fomit-frame-pointer
Borland C++

Se você encontrar uma melhor para algum desses compiladores ou um compilador diferente, por favor, deixe-me saber sobre isto.

* Uma pequena observação em não fazer um laço:
Pode parecer útil trocar a opção "loop unrolling" para otimizar (no compilador). Esta opção irá tentar converter pequenos laços em pedaços lineares de código. Minha experiência mostra, ao contrario, que está opção não produz nenhuma melhoria de performance. Marcando esta opção pode também fazer seu código parar em alguns casos especiais.
Optimizando o código C é mais fácil do que escolher opções no compilador, e geralmente depende da CPU para qual você compilou o código. Muitas regras gerais tendem aplicar para todos os CPUs. Não aceite absolutas verdades, como a velocidade (milhagem) pode variar:

* Use um profiler ! (programa especializado em medir performance)
Rodar seu computador sobre um profiler decente(GPROF - imediatamente vem a cabeça) pode revelar um monte de coisas maravilhosas que antes você nunca suspeitou. Você pode encontrar pedaços insignificantes de código que são executados mais frequentemente do que o resto do programa e este pedaço ainda pode dimuninuir a velocidade do programa inteiro. Otimizar esses pedaços de código ou reescrever-los em linguagem assembly irá aumentar a performance.

* Fuja do C++
Esqueça qualquer recurso que irá forçar você compilar seu programa com C++ ao invés do C: o compilador C++ normalmente insere muita "sujeira" (como os cabeçalhos) para o código gerado.

* Tamanho dos inteiros
Tente usar somente inteiros do tamanho base suportado pelo CPU, tipos int são diferentes dos tipos short ou long. Isto irá reduzir um monte o código para o compilador converter entre os diferentes tipos de inteiro. It também poderá reduzir o tempo de acesso a memória, como algumas CPUs funcionam mais rápido quando esta escrevendo/lendo dados no tamanho base do CPU.

* Alocação dos registradores
Use tão poucas variaveis quanto for possivel em cada bloco e declare as mais usadas diretamente nos registradore (A maioria dos novos compiladores automáticamente coloca as variaveis dentro dos registradores). Isto faz mais sentido para CPUs com muitos registradores (PowerPC) do que as com poucos registros dedicados (Intel 80x86).

* Desfaça pequenos laços
Se você tiver pequenos laços que executam poucas vezes, é sempre uma boa ideia manualmente você desfazer esses laços em linhas de códigos lineares. Veja a acima sobre desfazemento automatico de laços.

* Deslocamenteo vs. multiplicação/divisão
Sempre use deslocamento quando você precisar multiplicar ou dividir por 2^n (base 2 elevado a qualquer número ,2,4,8,16,32,64,128,256...) (J/128==J>>7). Eles executam mais rápido na maioria dos CPUs. Também, use o operador bitwise And para obter o modulo em tais casos (J%128==J&0x7F).


O que é low/high-endianess?
Todas CPUs normalmente são dividdas em muitas classes, dependendo de como eles guardam dados na memória. Enquando há algumas especimes muito peculiares, a maioria cai entre duas classes:

* CPU's High-endian irão guardar dados dos bytes mais altos da palavra sempre em primeiro.
Por exemplo, se você guarda 0x12345678 em uma CPU, a memória irá parecer isto:

0 1 2 3
+--+--+--+--+
|12|34|56|78|
+--+--+--+--+

* CPU's Low-endian irão guardar dados nos bytes mais baixos sempre ocorrem em primeiro na palavra.
O exemplo irá ficar um quanto tanto differente em tal CPU:

0 1 2 3
+--+--+--+--+
|78|56|34|12|
+--+--+--+--+

Exemplos tipicos de cPUs high-endian são os 6809, Motorola 680x0, PowerPC, Sun SPARC.
CPUs Low-endian incluem 6502 e seu sucessor 65816, Zilog Z80, a maioria dos chips Intel (incluindo 8080 e 80x86), DEC Alpha, etc.

Quando estiver escrevendo um emulador, você precisa estar atento ao fato endianess de ambos seu emulador e a onde seu emulador irá ficar. Digamos que você deseje emulatr o CPU Z80 que é low-endianess. Ou seja, Z80 guarda a palavra de 16-bits dentro do mais baixo byte. Se você usa low-endian para isto, tudo occore natuaralmente. Se você usa um high-endian m ai terá alguns probemas.
Uma maneira de tratar o problema da endiness é dado abaixo:

typedef union
{

short W; /* Word access */

struct /* Byte access... */
{
#ifdef LOW_ENDIAN
byte l,h; /* ...in low-endian architecture */
#else
byte h,l; /* ...in high-endian architecture */
#endif
} B;

} word;

Como você pode ver, uma palavra pode ser acessado no seu todo usando W. A Cada hora seu emulador precisa acessar isto como bytes separados, você pode usar B.l e B.h que preserva a ordem.

Se seu programa irá ser compilado para diferentes plafaformas, você pode querer testar isso pra ver antes, compile com a flag correta. Aqui um modo pra fazer isto.

int *T;

T=(int *)"\01\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
if(*T==1) printf("This machine is high-endian.\n");
else printf("This machine is low-endian.\n");

Como fazer programas portaveis?
Para ser escrito.
Porque deveria fazer programas modulares?
A maioria dos sistemas de computadores são feitos de muitos chips e cada um executa uma certa parte do sistema. Assim, há uma CPU, um controlador de Video, um gerador de Som e assim em diante. Alguns desses chips pode ter suas próprias memorias e outros hardwares anexados a eles.
Um emulador tipico deveria repetir o projeto original do sistema implementando cada subsistema num modulo separado. Primeiro, isto fazer debuggar mais fácil e assim todos os bugs localizados no modulos.
Segundo, a arquitetura modular permite você reusar os modulos em outro emuladores. O hardware de computador é um tanto quanto padronizado: você pode esperar encontrar a mesma CPU ou controlador de video em diferentes modelos de computador. É muito mais fácil emular um chip que você já tenha implementado.
©1997-2000 Copyright by Marat Fayzullin [marat at server komkon dot org]

Nenhum comentário: