Criando uma máquina de arcade emulador. Parte 2

imagem

A primeira parte está aqui .

Desmontador do processador 8080


Conhecimento


Vamos precisar de informações sobre os opcodes e seus respectivos comandos. Ao procurar informações na Internet, você notará que há muitas informações mistas sobre o 8080 e o Z80. O Z80 seguiu o 8080 - ele executa todas as instruções do 8080 com os mesmos códigos hexadecimais, mas também possui instruções adicionais. Eu acho que, enquanto você deve evitar informações sobre o Z80, para não ficar confuso. Eu criei uma tabela opcode para o nosso trabalho, é aqui .

Cada processador possui um guia de referência escrito pelo fabricante. Geralmente é chamado de algo como "Manual do Ambiente do Programador". O manual do 8080 é chamado de Manual do usuário dos sistemas de microcomputador Intel 8080. Sempre foi chamado de "livro de dados", então também o chamarei. Consegui fazer o download da referência 8080 em http://www.datasheetarchive.com/ . Este PDF é uma digitalização de baixa qualidade; portanto, se você encontrar uma versão melhor, use-a.

Vamos começar e dar uma olhada na ROM do Space Invaders. (O arquivo ROM pode ser encontrado na Internet.) Eu trabalho no Mac OS X, então apenas uso o comando hexdump para visualizar seu conteúdo. Para mais trabalho, encontre o editor hexadecimal para sua plataforma. Aqui estão os primeiros 128 bytes do arquivo invaders.h:

$ hexdump -v invaders.h 0000000 00 00 00 c3 d4 18 00 00 f5 c5 d5 e5 c3 8c 00 00 0000010 f5 c5 d5 e5 3e 80 32 72 20 21 c0 20 35 cd cd 17 0000020 db 01 0f da 67 00 3a ea 20 a7 ca 42 00 3a eb 20 0000030 fe 99 ca 3e 00 c6 01 27 32 eb 20 cd 47 19 af 32 0000040 ea 20 3a e9 20 a7 ca 82 00 3a ef 20 a7 c2 6f 00 0000050 3a eb 20 a7 c2 5d 00 cd bf 0a c3 82 00 3a 93 20 0000060 a7 c2 82 00 c3 65 07 3e 01 32 ea 20 c3 3f 00 cd 0000070 40 17 3a 32 20 32 80 20 cd 00 01 cd 48 02 cd 13 ... 

Este é o início do programa Space Invaders. Cada número hexadecimal é um comando ou dados para o programa. Podemos usar uma referência ou outra informação de referência para entender o que esses códigos hexadecimais significam. Vamos explorar um pouco mais o código da imagem da ROM.

O primeiro byte deste programa é $ 00. Olhando para a tabela, vemos que é NOP, bem como os dois comandos a seguir. (Mas não desanime, os Space Invaders provavelmente usaram esses comandos como um atraso para deixar o sistema se acalmar um pouco depois de ligar a energia.)

O quarto comando é $ C3, ou seja, a julgar pela tabela, este é o JMP. A definição de um comando JMP indica que ele recebe um endereço de dois bytes, ou seja, os próximos dois bytes são o endereço de salto do JMP. Então mais dois NOPs chegam ... então, você sabe o que? Deixe-me assinar as primeiras instruções pessoalmente ...

  0000 00 NOP 0001 00 NOP 0002 00 NOP 0003 c3 d4 18 JMP $18d4 0006 00 NOP 0007 00 NOP 0008 f5 PUSH PSW 0009 c5 PUSH B 000a d5 PUSH D 000b e5 PUSH H 000c c3 8c 00 JMP $008c 000f 00 NOP 0010 f5 PUSH PSW 0011 c5 PUSH B 0012 d5 PUSH D 0013 e5 PUSH H 0014 3e 80 MVI A,#0x80 0016 32 72 20 STA $2072 

Parece haver alguma maneira de automatizar esse processo ...

Desmontador, Parte 1


Um desmontador é um programa que simplesmente converte um fluxo de números hexadecimais de volta para o código-fonte na linguagem assembly. Essa é exatamente a tarefa que realizamos manualmente na seção anterior - uma ótima oportunidade para automatizar este trabalho. Ao escrever esse trecho de código, nos familiarizamos com o processador e obtemos um trecho conveniente de código de depuração, que é útil ao escrever um emulador de CPU.

Aqui está o algoritmo de desmontagem do código 8080:

  1. Leia o código no buffer
  2. Recebemos um ponteiro para o início do buffer
  3. Use o byte no ponteiro para determinar o código de operação.
  4. Exiba o nome do código de operação, se necessário, usando bytes após o código de operação como dados
  5. Mova o ponteiro para o número de bytes usados ​​por este comando (1, 2 ou 3 bytes)
  6. Se o buffer não terminar, vá para a etapa 3

Para estabelecer a base do procedimento, adicionei algumas instruções abaixo. Vou descrever o procedimento completo para o download, mas recomendo que você tente escrevê-lo. Não demorará muito tempo e, em paralelo, você aprenderá o conjunto de instruções do processador 8080.

  /* *codebuffer -       8080 pc -          */ int Disassemble8080Op(unsigned char *codebuffer, int pc) { unsigned char *code = &codebuffer[pc]; int opbytes = 1; printf ("%04x ", pc); switch (*code) { case 0x00: printf("NOP"); break; case 0x01: printf("LXI B,#$%02x%02x", code[2], code[1]); opbytes=3; break; case 0x02: printf("STAX B"); break; case 0x03: printf("INX B"); break; case 0x04: printf("INR B"); break; case 0x05: printf("DCR B"); break; case 0x06: printf("MVI B,#$%02x", code[1]); opbytes=2; break; case 0x07: printf("RLC"); break; case 0x08: printf("NOP"); break; /* ........ */ case 0x3e: printf("MVI A,#0x%02x", code[1]); opbytes = 2; break; /* ........ */ case 0xc3: printf("JMP $%02x%02x",code[2],code[1]); opbytes = 3; break; /* ........ */ } printf("\n"); return opbytes; } 

No processo de escrever esse procedimento e estudar cada código de operação, aprendi muito sobre o processador 8080.

  1. Percebi que a maioria das equipes leva um byte, o restante dois ou três. O código acima pressupõe que o comando tenha um byte de tamanho, mas as instruções de dois e três bytes alteram o valor da variável “opbytes” para que o tamanho correto do comando seja retornado.
  2. O 8080 possui registros com os nomes A, B, C, D, E, H e L. Há também um contador de programa (contador de programa, PC) e um ponteiro de pilha separado (ponteiro de pilha, SP).
  3. Algumas instruções funcionam com registradores em pares: B e C são um par, além de DE e HL.
  4. A é um registro especial, muitas instruções funcionam com ele.
  5. HL também é um registro especial, é usado como endereço para cada leitura e gravação de dados na memória.
  6. Fiquei curioso sobre a equipe “RST”, então li o guia um pouco. Notei que ele executa o código em locais fixos e a referência menciona o tratamento de interrupções. Após uma leitura mais aprofundada, verificou-se que todo esse código no início da ROM era rotinas de serviço de interrupção (ISRs). As interrupções podem ser geradas programaticamente usando o comando RST ou geradas por fontes de terceiros (não pelo processador 8080).

Para transformar tudo isso em um programa de trabalho, acabei de criar um procedimento que executa as seguintes etapas:

  1. Abre um arquivo preenchido com o código compilado 8080
  2. Lê no buffer de memória
  3. Passa pelo buffer de memória, causando Disassemble8080Op
  4. Aumenta o PC retornado por Disassemble8080Op
  5. Saídas no final do buffer

Pode ser algo como isto:

  int main (int argc, char**argv) { FILE *f= fopen(argv[1], "rb"); if (f==NULL) { printf("error: Couldn't open %s\n", argv[1]); exit(1); } //         fseek(f, 0L, SEEK_END); int fsize = ftell(f); fseek(f, 0L, SEEK_SET); unsigned char *buffer=malloc(fsize); fread(buffer, fsize, 1, f); fclose(f); int pc = 0; while (pc < fsize) { pc += Disassemble8080Op(buffer, pc); } return 0; } 

Na segunda parte, examinaremos a saída obtida desmontando os ROM Space Invaders.

Alocação de memória


Antes de começarmos a escrever um emulador de processador, precisamos estudar outro aspecto. Todas as CPUs têm a capacidade de se comunicar com um certo número de endereços. Os processadores mais antigos tinham endereços de 16, 24 ou 32 bits. O 8080 possui 16 contatos de endereço, portanto, os endereços estão no intervalo de 0 a $ FFFF.

Para entender a alocação de memória do jogo, precisamos realizar uma pequena investigação. Depois de coletar as informações aqui e aqui , descobri que a ROM está localizada no endereço 0 e o jogo tem 8 KB de RAM a partir de US $ 2000.

O autor de uma das páginas descobriu que o buffer de vídeo inicia na RAM com um endereço de US $ 2.400 e também nos contou como as portas de entrada e saída 8080 são usadas para se comunicar com controles e equipamentos de áudio. Ótimo!

Dentro do arquivo ROM invaders.zip, que pode ser encontrado na Internet, existem quatro arquivos: invaders.e, .f, .g e .h. Após pesquisar no Google, deparei-me com um artigo informativo que explica como colocar esses arquivos na memória:

Space Invaders, (C) Taito 1978, Midway 1979

: Intel 8080, 2 ( Zilog Z80)

: $cf (RST 8) vblank, $d7 (RST $10) vblank.

: 256(x)*224(y), 60 , .
.
: 7168 , 1 (32 ).

: SN76477 .

:
ROM
$0000-$07ff: invaders.h
$0800-$0fff: invaders.g
$1000-$17ff: invaders.f
$1800-$1fff: invaders.e

RAM
$2000-$23ff:
$2400-$3fff:

$4000-:


Ainda há algumas informações úteis, mas ainda não estamos prontos para usá-las.

Detalhes sangrentos


Se você quiser saber qual o tamanho do espaço de endereço do processador, poderá entender isso examinando suas características. A especificação 8080 nos diz que o processador possui 16 contatos de endereço, ou seja, usa endereçamento de 16 bits. (Em vez de especificações, basta ler o manual, Wikipedia, google e assim por diante ...)

Na Internet, existem muitas informações sobre o hardware do Space Invaders. Se você não conseguiu encontrar essas informações, pode obtê-las de duas maneiras:

  • Assista o código em execução no emulador e descubra o que ele faz. Faça anotações e observe com cuidado. Deve ser simples o suficiente para entender, por exemplo, onde, na opinião do jogo, a RAM deve estar localizada. Também é fácil determinar o local em que ela está procurando memória de vídeo (passaremos algum tempo estudando isso).
  • Encontre o diagrama de circuito da máquina de fliperama e acompanhe os sinais dos contatos de endereço da CPU. Veja para onde eles estão indo. Por exemplo, A15 (endereço mais antigo) só pode ir para a ROM. A partir disso, podemos concluir que os endereços da ROM começam em US $ 8000.

Pode ser muito interessante e informativo descobrir você mesmo observando a execução do código. Alguém teve que lidar com tudo isso pela primeira vez.

Desenvolvimento da linha de comando


O objetivo deste tutorial não é ensiná-lo a escrever código para uma plataforma específica, embora não possamos evitar código específico da plataforma. Espero que, antes do início do projeto, você já saiba compilar para sua plataforma de destino.

Quando você trabalha com código independente, que simplesmente lê arquivos e exibe texto no console, não é necessário usar algum sistema de desenvolvimento muito complicado. De fato, isso apenas complica as coisas. Tudo que você precisa é de um editor de texto e terminal.

Eu acho que quem quer programar em um nível baixo deve saber como criar programas simples a partir da linha de comando. Você pode considerar que eu o provoco, mas suas habilidades de hackers de elite não valem muito se você não puder funcionar fora do Visual Studio.

No Mac, você pode usar o TextEdit e o Terminal para compilar. No Linux, você pode usar o gedit e o Konsole. No Windows, você pode instalar o cygwin e ferramentas e, em seguida, usar o N ++ ou outro editor de texto. Se você quer ser realmente legal, todas essas plataformas suportam vi e emacs para edição de texto.

Compilar programas de um único arquivo usando a linha de comando é uma tarefa trivial. Suponha que você salvou seu programa em um arquivo chamado 8080dis.c . Vá para a pasta com este arquivo de texto e compile-o assim: cc 8080dis.c . Se você não especificar o nome do arquivo de saída, ele será chamado de a.out e você poderá executá-lo digitando ./a.out .

Isso, de fato, é tudo.

Usando um depurador


Se você estiver trabalhando em um dos sistemas baseados em Unix, segue uma breve introdução à depuração de programas de linha de comando usando o GDB. Você precisa compilar o programa assim: cc -g -O0 8080dis.c . O parâmetro -g gera informações de depuração (ou seja, você pode executar a depuração com base no texto de origem), e o parâmetro -O0 desativa as otimizações para que, ao percorrer o programa, o depurador possa rastrear com precisão o código de acordo com o texto de origem.

Aqui está o log anotado do início de uma sessão de depuração. Meus comentários estão em linhas marcadas com um sinal de libra (#).

  $ gdb a.out GNU gdb 6.3.50-20050815 (Apple version gdb-1708) (Mon Aug 8 20:32:45 UTC 2011) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done #  ,       (gdb) b Disassemble8080Op Breakpoint 1 at 0x1000012ef: file 8080dis.c, line 7. #   "invaders.h"    (gdb) run invaders.h Starting program: /Users/bob/Desktop/invaders/a.out invaders.h Reading symbols for shared libraries +........................ done Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=0) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; #gdb  n  "next".    "next" (gdb) n 8 int opbytes = 1; #p -    "print",     *code (gdb) p *code $1 = 0 '\0' (gdb) n 9 printf("%04x ", pc); #    "", gdb     ,    "next" (gdb) 10 switch (*code) (gdb) n #   ,    "NOP" 12 case 0x00: printf("NOP"); break; (gdb) n 285 printf("\n"); #c -  "continue",        (gdb) c Continuing. 0000 NOP #     Disassemble8080Op.   *opcode, # ,      NOP,    . Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=1) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) c Continuing. 0001 NOP Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=2) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) n 8 int opbytes = 1; (gdb) p *code $2 = 0 '\0' #  NOP,   (gdb) c Continuing. 0002 NOP Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=3) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) n 8 int opbytes = 1; #   ! (gdb) p *code $3 = 195 '?' # print     ,    /x    (gdb) p /x *code $4 = 0xc3 (gdb) n 9 printf("%04x ", pc); (gdb) 10 switch (*code) (gdb) # C3 -  JMP. . 219 case 0xc3: printf("JMP $%02x%02x",code[2],code[1]); opbytes = 3; break; (gdb) 285 printf("\n"); 

Desmontador, Parte 2


Execute o desmontador para o arquivo ROM invaders.h e observe as informações exibidas.

  0000 NOP 0001 NOP 0002 NOP 0003 JMP $18d4 0006 NOP 0007 NOP 0008 PUSH PSW 0009 PUSH B 000a PUSH D 000b PUSH H 000c JMP $008c 000f NOP 0010 PUSH PSW 0011 PUSH B 0012 PUSH D 0013 PUSH H 0014 MVI A,#$80 0016 STA $2072 0019 LXI H,#$20c0 001c DCR M 001d CALL $17cd 0020 IN #$01 0022 RRC 0023 JC $0067 0026 LDA $20ea 0029 ANA A 002a JZ $0042 002d LDA $20eb 0030 CPI #$99 0032 JZ $003e 0035 ADI #$01 0037 DAA 0038 STA $20eb 003b CALL $1947 003e SRA A 003f STA $20ea /* 0000000 00 00 00 c3 d4 18 00 00 f5 c5 d5 e5 c3 8c 00 00 0000010 f5 c5 d5 e5 3e 80 32 72 20 21 c0 20 35 cd cd 17 0000020 db 01 0f da 67 00 3a ea 20 a7 ca 42 00 3a eb 20 0000030 fe 99 ca 3e 00 c6 01 27 32 eb 20 cd 47 19 af 32 */ 

As primeiras instruções correspondem àquelas que escrevemos manualmente anteriormente. Depois deles, há várias novas instruções. Abaixo eu inseri dados hexadecimais para referência. Observe que se você comparar a memória com os comandos, os endereços serão armazenados na memória na ordem inversa. Assim é. Isso é chamado little endian - máquinas com pouco endian, como o 8080, armazenam os bytes menos significativos de números primeiro. (Mais informações sobre endian estão descritas abaixo.)

Eu mencionei acima que esse código é o código ISR para o jogo Space Invaders. O código para interrupções 0, 1, 2, ... 7 começa com o endereço $ 0, $ 8, $ 20, ... $ 38. Parece que o 8080 fornece apenas 8 bytes para cada ISR. Às vezes, o programa Space Invaders ignora esse sistema simplesmente movendo-se para outro endereço com mais espaço. (Isso acontece em US $ 000c).

Além disso, o ISR 2 parece ser maior que a memória alocada para ele. Seu código vai para $ 0018 (este é o local para ISR 3). Eu acho que os Space Invaders não esperam ver nada que use a interrupção 3.

O arquivo ROM do Space Invaders da Internet consiste em quatro partes. Vou explicar isso abaixo, mas, por enquanto, para avançar para a próxima seção, precisamos mesclar esses quatro arquivos em um. No Unix:

  cat invaders.h > invaders cat invaders.g >> invaders cat invaders.f >> invaders cat invaders.e >> invaders 

Agora execute o desmontador com o arquivo "invasores" resultante. Quando um programa começa em US $ 0000, a primeira coisa a fazer é mudar para US $ 18d4. Considerarei isso o início do programa. Vamos dar uma olhada rápida neste código.

  18d4 LXI SP,#$2400 18d7 MVI B,#$00 18d9 CALL $01e6 

Portanto, ele executa duas operações e chama $ 01e6. Vou inserir parte do código com transições neste código:

  01e6 LXI D,#$1b00 01e9 LXI H,#$2000 01ec JMP $1a32 ..... 1a32 LDAX D 1a33 MOV M,A 1a34 INX H 1a35 INX D 1a36 DCR B 1a37 JNZ $1a32 1a3a RET 

Como vimos na alocação de memória dos Space Invaders, alguns desses endereços são interessantes. $ 2000 é o começo de um programa de “RAM de trabalho”. US $ 2.400 é o começo da memória de vídeo.

Vamos adicionar comentários ao código para explicar o que ele faz diretamente na inicialização:

  18d4 LXI SP,#$2400 ; SP=$2400 -      18d7 MVI B,#$00 ; B=0 18d9 CALL $01e6 ..... 01e6 LXI D,#$1b00 ; DE=$1B00 01e9 LXI H,#$2000 ; HL=$2000 01ec JMP $1a32 ..... 1a32 LDAX D ; A = (DE),   ,       $1B00 1a33 MOV M,A ;  A  (HL),     $2000 1a34 INX H ; HL = HL + 1 ( $2001) 1a35 INX D ; DE = DE + 1 ( $1B01) 1a36 DCR B ; B = B - 1 ( 0xff,      0) 1a37 JNZ $1a32 ; ,   ,     b=0 1a3a RET 

Parece que esse código copiará 256 bytes de $ 1b00 a $ 2000. Porque Eu não sei Você pode estudar o programa com mais detalhes e refletir sobre o que ele faz.

Há um problema aqui. Se tivermos um pedaço de memória arbitrário contendo código, os dados provavelmente se alternarão com ele.

Por exemplo, sprites para personagens do jogo podem ser misturados com o código. Quando um desmontador cai em um fragmento de memória, ele pensa que isso é código e continua a "mastigá-lo". Se você tiver azar, qualquer código desmontado após esse dado pode estar incorreto.

Embora dificilmente possamos fazer algo sobre isso. Basta ter em mente que esse problema existe. Se você vir algo assim:

  • transição de um código exatamente bom para uma equipe que não está na lista de desmontadores
  • fluxo de código sem sentido (por exemplo, POP B POP B POP B POP C XTHL XTHL XTHL)

aqui, provavelmente, existem dados que arruinaram parte do código desmontado. Se isso acontecer, você precisará iniciar novamente a partir do deslocamento.

Acontece que os invasores do espaço periodicamente encontram zeros. Se nossa desmontagem parar, os zeros o forçarão a executar um reset.

Uma análise detalhada do código dos Space Invaders pode ser encontrada aqui .

Endian


Os bytes são armazenados de maneira diferente em diferentes modelos de processadores, e o armazenamento depende do tamanho dos dados. Máquinas big-endian armazenam dados de mais velhos para mais jovens. Little-endian mantê-los do mais jovem para o mais velho. Se um número inteiro de 32 bits 0xAABBCCDD for gravado na memória de cada máquina, ele terá a seguinte aparência:

Em little-endian: $ DD $ CC $ BB $ AA

Big endian: $ AA $ BB $ CC $ DD

Comecei a programar em processadores Motorola que usavam big endian, então me pareceu mais "natural", mas depois me acostumei com o little endian.

Meu desmontador e emulador evitam completamente o problema endian porque eles lêem apenas um byte de cada vez. Se você quiser, por exemplo, usar um leitor de 16 bits para ler o endereço da ROM, observe que esse código não é portátil entre as arquiteturas da CPU.

Source: https://habr.com/ru/post/pt418683/


All Articles