domingo, 20 de fevereiro de 2011

Como criar um emulador: tutorial avançado para quem quizer criar um emulador

Conteúdo

Então você decidiu  escrever um  software de emulador?  Muito bem,
pois  este documento responde  às perguntas técnicas comuns  sobre
como escrever emuladores, podendo assim lhe ajudar em algo.

     O que pode ser emulado?
     O que é "emulação" e o que a difere de "simulação"?
     É legal emular o hardware de um proprietário?
     O que é "interpretar o emulador" e o que o difere de
       "recompilar o emulador"?
     Eu desejo escrever um emulador. Por onde devo começar?
     Que linguagem de programação eu devo usar?
     Onde eu obtenho informações do hardware a ser emulado?
     Como eu emulo uma CPU?
     Como eu optimizo meu código C?
     O resto está por vir...


# O que pode ser emulado?

Basicamente qualquer  coisa  que tenha um microprocessador dentro.
Certamente,  os dispositivos mais interessantes para se emular são
os que funcionam com mais ou menos flexibilidade. Incuindo:

     Computadores
     Calculadoras
     Consoles de Videogames
     Arcades de Videogames
     etc.

É sempre  bom  lembrar  que você  pode emular  qualquer sistema de
computador, mesmo os mais complexos ( como o computador  Commodore
Amiga, por exemplo). Porém, nestes casos a performance da emulação
pode ser muito baixa.


# O que é "emulação" e o que a difere de "simulação"?

Emulação  é  a  tentativa  de  imitar  o  desenho  interior de  um
dispositivo. Simulação é a tentativa  de imitar  as  funções de um
dispositivo. Exemplos: um programa que imita o hardware  do arcade
de Pacman para rodar a verdadeira  ROM  de Pacman é um emulador. O
jogo  do Pacman escrito para um computador usar gráficos parecidos
com o real arcade é um simulador.


# É legal emular o hardware de um proprietário?

Embora muitos digam o contrário, ao que me parece é legal emular o
hardware  de  um  proprietário, se  as  informações dele não forem
obtidas por meios ilegais. Você deve estar atento ao fato de que é
ilegal  distribuir ROMs de sistema ( BIOS, etc. ) com emulador, se
este  estiver em  estado  "copyrighted" (cópia registrada).


# O que é "interpretar o emulador" e o que o difere de "recompilar
  o emulador"?

Há três esquemas básicos que podem ser utilizados por um emulador.
Eles podem ser combinados para se obter um melhor resultado.

     Interpretação

     O emulador  lê o código emulado  da memória  byte-por-byte, o
decodifica  e  desempenha  os comandos  apropriados  nos registros
emulados, memória, e I/O. O algoritimo geral de tal emulador  é  o
seguinte:


     while(CPUIsRunning)
     {
       Fetch OpCode
       Interpret OpCode
     }

     O melhor deste código é a facilidade do debug,  portabilidade
e  sincronização  ( você pode simplesmente contar os ciclos  que o
relógio faz  e  dar descanso  à  emulação por conta do ciclo ).  O
simples, grande e óbvio problema é a performance.  A interpretação
toma muito de tempo da CPU, e você pode  precisar de um computador
rápido para rodar seu código numa velocidade decente.

     Recompilação Estática

     Nesta técnica, você pega o programa escrito no código emulado
e tenta traduzí-lo para o código de montagem  (código assembly) de
seu computador.  O resultado  será um usual arquivo executável que
você  poderá rodar no seu computador  sem nenhuma outra ferramenta
especial.
     Apesar de recompilação  estática soar bem melhor,  nem sempre
ela  será   possível.   Por exemplo,  você   não  pode  recompilar
estaticamente um código auto-modificado (self-modifying), pois não
existe jeito  de  faze-lô rodar.  Para evitar tais situações, você
pode  tentar combinar a recompilação  estática com a interpretação
ou recompilação dinâmica.

     Recompilação Dinâmica

     A recompilação  dinâmica  é  essencialmente a mesma coisa que
a estática, a diferença é que ocorre durante execução do programa.
Ao  invés de tentar  recompilar todo o código em uma só vez,  você
vai aonde quer com as instruções CALL ou JUMP.
Para aumentar a velocidade,  esta técnica deve ser combinada com a
recompilação  estática.  Você pode ler mais  sobre a  recompilação
dinâmica  no  white paper (papel branco),  por Ardi,  criadores da
recompilação do emulador de Macintosh.


# Eu desejo escrever um emulador. Por onde devo começar?

Para escrever um emulador, você deve ter um bom conhecimento geral
de  programação de computadores  e  de eletrônica digital.  Alguma
experiência em programação em assembly lhe dará uma boa mão.

 1.Escolha uma linguagem de programação para usar.
 2.Ache toda informação disponível sobre o hardware a ser emulado.
 3.Escreva a emulação  da CPU ou obtenha o código real da emulação
   da CPU.
 4.Faça algum esboço do código para emular o descanso do hardware,
   ao menos parcialmente.
 5.Agora,  será  muito útil escrever um pequeno debugger embutido,
   que  permita  parar  a  emulação e ver  o  que  o programa está
   fazendo.  Talvez você precise de um disassembler da emulação da
   linguagem  do  sistema assembly.  Escreva  o seu próprio se não
   exitir um destes.
 6.Tente rodar programas no seu emulador.
 7.Use  o disassembler e  o debugger para ver que programas usam o
   hardware e ajuste seu código apropriadamente.



# Que linguagem de programação eu devo usar?

As alternativas mais óbvias são C e Assembly. Aqui estão os prós e
contras de cada uma delas:

     Assembly

     + Geralmente permite produzir um código rápido.
     + Os  registros  emulados  da  CPU podem  ser utilizados para
       armazenar diretamente os registros da CPU emulada.
     + Muitos opcodes podem  ser emulados com os opcodes similares
       à CPU emulada.
     - O código não é portátil, ou seja, não será possível rodá-lo
       num computador com arquitetura diferente.
     - É difícil o debug e a manutenção do código.

     C

     + O código pode se tornar  portátil se trabalhar em  sistemas
       operacionais e computadores diferentes.
     + É relativelmente fácil o debug e a manutenção do código.
     + As  diferentes  hipóteses de  como trabalha o hardware real
       podem ser testadas rapidamente.
     - C é, geralmente, mais lento que o código assembly puro.

Bom conhecimento da linguagem escolhida é absolutamente necessário
para se escrever um emulador que funcione, assim como, num projeto
complexo  como este,  o código deveria  ser  optimizado para poder
rodar o mais rápido possível.
Emulação de computadores definitivamente não  é um dos projetos em
que você possa aprender uma linguagem de programação.


# Onde eu obtenho informações do hardware a ser emulado?

A seguir há uma lista de lugares onde você pode achar o que quer.

NEWSGROUPS (Grupos de Notícias)

  comp.emulators.misc
     Este é um grupo de notícias de discussão geral sobre emulação
de computadores.  Muitos autores do emuladores lêem ele,  embora o
nível de barulho seja muito alto.

  comp.emulators.game-consoles
     O mesmo  que  o comp.emulators.misc, só que mais especificado
na emulação de consoles de videogames.

  comp.sys./emulated-system/
     A hierarquia comp.sys.* contém grupos de notícias dedicados à
computadores específicos. Você pode obter muita informação técnica
útil na leitura desses grupos. Exemplos típicos:

  comp.sys.msx       Computadores MSX/MSX2/MSX2+/TurboR
  comp.sys.sinclair  Sinclair ZX80/ZX81/ZXSpectrum/QL
  comp.sys.apple2    Apple ][
  etc.

     Por favor, cheque sempre as FAQS apropriadas
     antes de postar em qualquer grupo.
  alt.folklore.computers
  rec.games.video.classic

FTP

Oulu
ftp://x2ftp.oulu.fi/pub


WWW

Emulation Programmers Resource
http://www.classicgaming.com/EPR/

Emulation (emulação de macintosh)
http://www.emulation.net/

Programmer Heaven
http://www.programmershaven.com/

Komkon
http://www.komkon.org/


FAQ

Dê uma olhada nos sites (WWW) e nos grupos de notícias


# Como eu emulo uma CPU?

Antes de tudo, se você apenas necessitar emular o padrão Z80  ou a
CPU 6502,  você  deverá  usar um dos emuladores  de  CPU que eu já
citei. Devem ser aplicadas certas condições para seu uso correto.
Para que deseja escrever  seu próprio código de emulação de CPU ou
está interessado  em saber como ele trabalha,  eu forneço o código
básico  de um emulador de CPU típico em linguagem C,  logo abaixo.
Para o seu  próprio  emulador,  você  pode  querer retirar algumas
partes ou adicionar outras para personalizá-lo.


Counter=InterruptPeriod;
PC=InitialPC;

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

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

  if(Counter<=0) {
    /* checa por interrupções e faz outra emulação do hardware */
      ...
    counter+="InterruptPeriod;"
    if(exitrequired) break;
  }
}


Primeiro, nós designamos os  valores iniciais ao contador de ciclo
da CPU (Counter) e ao contador do programa (PC):

Counter=InterruptPeriod;
PC=InitialPC;

O contador  contém  o número de ciclos que a CPU deixa à próxima -
suspeitada - interrupção.  Note que a interrupção não deve ocorrer
necessariamente  quando  o contador  expirar:  você pode usar isto
para  muitos  outros propósitos,  tal como sincronizar timers,  ou
atualizar os scanlines da tela. Mais deixe isto pra mais tarde.  O
PC contém uma memória endereçada,  que a nossa CPU emulada lerá no
seu próximo opcode.

Depois  que  os  valores iniciais são designados,  nós iniciamos o
loop principal:

for(;;)
{

Note que este loop também pode ser implementado com:

while(CPUIsRunning)
{

Aqui, CPUIsRunning é uma variável boolean. Ela tem suas vantagens,
como a  de  você poder terminar o loop a qualquer momento, setando
CPUIsRunning=0.  Infelizmente, checando esta variável toda vez que
ela  passar, tomará  muito  de  tempo da CPU. Seria bom que você a
evita-se quando possível, porém, não implemente este loop com:

while(1)
{

Porque, neste caso, alguns compiladores gerarão um código checando
se 1 é verdadeiro ou não. Você certamente não deseja um compilador
fazendo um trabalho desnecessário em toda passagem de um loop.
Agora nós estamos no loop, e  a  primeira coisa será ler o próximo
opcode, que modifica o contador de programa:

OpCode=Memory[PC++];

Este é o caminho mais simples e rápido para ler a memória emulada,
porém, ela não será possível pelas seguintes razões:

 × Memória  pode ser  fragmentada  dentro  das  páginas  de switch
  (bancos aka)
 × Lá podem ser mapeadas a memória dos dispositivos I/O do sistema

Nestes  casos,  nós  podemos  ler  a  memória  emulada pela função
ReadMemory():

OpCode=ReadMemory(PC++);

Geralmente  a  função WriteMemory() serve para escrever na memória
emulada. Além disso, se tratando de memória I/O mapeada e páginas,
o WriteMemory() pode fazer também o seguinte:

  Protejer a ROM contra modificações
     Alguns softwares baseados em cartuchos (tais como os jogos de
MSX, por exemplo)  tentam escrever na  pópria ROM  e  se recusam a
trabalhar se são bem sucedidos. Isto  é  freqüentemente feito para
se protejer a cópia.

  Manejar a memória espelhada (mirrored memory)
     Uma área  de  memória pode  ser acessível de vários endereços
diferentes.  Por   exemplo,  os  dados  que  você  escreve  dentro
localização  $4000  também  pode  aparecer  em $6000 e $8000. Esta
situação  pode  ser  resolvida  com  o  uso  do ReadMemory(), mas,
usualmente, ela não  é  desejável, já que o ReadMemory() obtêm uma
chamada/call muito mais freqüente que com o WriteMemory().
Portanto, o  caminho mais eficiente deve ser implementar a memória
espelhando na função WriteMemory().

As  funções  ReadMemory()/WriteMemory()  dão  muita  sobrecarga na
emulação, sendo que seu dever é a deixar mais  eficiente possível,
porque elas obterão chamadas/calls muito freqüentemente. Aqui  vai
um exemplo destas funções:

   leitura da memória (ReadMemory) do byte em inline estático
   (registra o endereço da palavra):

{
  return(MemoryPage[Address>>13][Address&0x1FFF]);
}

   leitura da memória (ReadMemory) vazia em inline estático
   (registra o endereço da palavra e o valor do byte):

{
  MemoryPage[Address>>13][Address&0x1FFF]=Value;
}

Note  a  palavra-chave inline.  Ela contará com um compilador para
embutir  a  função  dentro  do  código,  ao  invés de fabricar uma
chamada/call para isto.
Se seu compilador  não suportar inline ou _inline, tente  fabricar
uma  função  estática: alguns compiladores  (WatcomC, por exemplo)
optimizam funções estáticas curtas por inlining.

Também, lembre-se de que na maioria dos casos, o ReadMemory() será
chamado muitas vezes mais freqüentemente que o WriteMemory().
Portanto, isto  estará valendo para implementar um código maior no
WriteMemory(), guardando ReadMemory() para os mais simples e curto
possíveis.

Depois que trouxer o opcode nós diminuímos o contador de ciclos da
CPU pelo número de ciclos requeridos por este opcode:

Counter-=Cycles[OpCode];

A  tabela  Cycles[]  deverá  conter o número de ciclos da CPU para
cada  opcode. Tome  cuidado,  pois  alguns opcodes (tal como jumps
condicionais ou chamadas/calls do subrotina) podem tomam um número
diferente de ciclos dependendo  de  seus argumentos. Isto pode ser
ajustado mais tarde, com outro código.

Agora está na hora de interpretar o opcode e executá-lo:

switch(OpCode)
{

É  comum  ter  a  concepção  de  que a construção de um switch() é
ineficiente  para  compilar  numa cadeia  de  declarações if() ...
else if() ... Enquanto isso  é  verdadeiro para construções com um
pequeno número de casos, as grandes construções ( 100-200  ou mais
casos )  sempre  parecem  compilar  dentro  uma  tabela  de jumps,
deixando eles completamente eficientes.

Há  dois  caminhos  alternativos  para  se interpretar os opcodes.
O  primeiro  vai  fazer  uma   tabela de  funções e chamar a  mais
apropriada. Este método aparece ser menos eficiente que o switch()
pois  você  irá receber uma sobrecarga de funções de chamada/call.
O segundo método é fazer uma tabela de labels, e usar a declaração
do goto.  Enquanto  este  método  é ligeiramente mais rápido que o
switch(), ele  trabalhará unicamente nos compiladores que suportam
"precomputed labels" (labels pré computadas). Outros  compiladores
não permitirão que você crie um arranjo de endereços de labels.

Depois  da  bem  sucedida  interpretação  e execução dum opcode, é
chegada a hora de checar se é necessária alguma interrupção. Neste
momento,  você  também  pode  desempenhar  quaisquer  tarefas  que
necessitam ser sincronizadas com o relógio do sistema:

if(Counter<=0)
{
  /* checa por interrupções e faz outra emulação de hardware */
  ...
  counter+="InterruptPeriod;"
  if(exitrequired) break;
}

A seguir, uma  lista  curta  de  coisas  que você pode fazer com a
declaração if():

  Checar  se  o  fim da tela  é  alcançado e gerar uma interrupção
    VBlank se assim
  Checar se o fim  do scanline é alcançado e gerar uma interrupção
    HBlank se assim
  Checar  por  uma  colisão de sprites, e gerar uma interrução, se
    necessário
  Atualizar os  timers  da emulação de hardware, gerar uma
    interrupção se os timers expirarem
  Refrescar a amostra/display de scanline
  Refrescar o modo tela inteira
  Atualizar som
  Ler o estado do teclado/joysticks
  etc.

Cuidadosamente calcule o número de ciclos que a CPU necessita para
cada tarefa, então use o menor número no InterruptPeriod, e  junte
todas  as  outras  tarefas  nele ( elas  não devem necessariamente
executar em toda expiração do contador ).

Note que nós não apenas designamos  Contador=InterruptPeriod,  mas
fizemos um Counter+=InterruptPeriod: ele fará o contador de ciclos
mais preciso, mesmo usando números negativos.

Também dê uma olhada na linha

if(ExitRequired) break;

Como fica  muito  custoso checar a saída em toda passagem do loop,
nós fazemos isto unicamente quando o Contador expirar: ele  apenas
sairá da emulação quando você atribuir ExitRequired=1, não tomando
muito tempo da CPU.

Isto é tudo que eu tenho a dizer sobre emulação de CPU em C.
Você   deve  ser   capaz  de  imaginar   quanto  trabalho  eu  lhe
economizei.

Créditos: Emulabr







Nenhum comentário:

Postar um comentário