Creación de una máquina arcade emulador. Parte 2

imagen

La primera parte está aquí .

Desmontador del procesador 8080


Conocido


Necesitaremos información sobre los códigos de operación y sus respectivos comandos. Cuando busque información en Internet, notará que hay mucha información mixta sobre el 8080 y el Z80. El Z80 era un seguidor del 8080: ejecuta todas las instrucciones 8080 con los mismos códigos hexadecimales, pero también tiene instrucciones adicionales. Creo que, si bien debe evitar la información sobre el Z80, para no confundirse. Creé una tabla de código de operación para nuestro trabajo, está aquí .

Cada procesador tiene una guía de referencia escrita por el fabricante. Por lo general, se llama algo así como "Manual del entorno del programador". El manual 8080 se llama Manual de usuario de sistemas de microordenador Intel 8080. Siempre se le ha llamado un "libro de datos", así que también lo llamaré así. Pude descargar la referencia 8080 de http://www.datasheetarchive.com/ . Este PDF es un escaneo de baja calidad, así que si encuentra una versión mejor, úsela.

Comencemos y echemos un vistazo a la ROM de Space Invaders. (El archivo ROM se puede encontrar en Internet). Trabajo en Mac OS X, así que solo uso el comando hexdump para ver su contenido. Para más trabajo, encuentre el editor hexadecimal para su plataforma. Aquí están los primeros 128 bytes del archivo 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 es el comienzo del programa Space Invaders. Cada número hexadecimal es un comando o datos para el programa. Podemos usar una referencia u otra información de referencia para comprender lo que significan estos códigos hexadecimales. Exploremos un poco más el código de la imagen ROM.

El primer byte de este programa es $ 00. Mirando la tabla, vemos que es NOP, así como los siguientes dos comandos. (Pero no se desanime, Space Invaders probablemente utilizó estos comandos como un retraso para permitir que el sistema se calme un poco después del encendido).

El cuarto comando es $ C3, es decir, a juzgar por la tabla, esto es JMP. La definición de un comando JMP establece que recibe una dirección de dos bytes, es decir, los siguientes dos bytes son la dirección de salto JMP. Luego vienen dos NOP más ... así que, ¿sabes qué? Permítanme firmar las primeras instrucciones yo mismo ...

  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 que hay alguna forma de automatizar este proceso ...

Desensamblador, Parte 1


Un desensamblador es un programa que simplemente traduce una secuencia de números hexadecimales al código fuente en lenguaje ensamblador. Esta es exactamente la tarea que realizamos a mano en la sección anterior: una gran oportunidad para automatizar este trabajo. Al escribir este fragmento de código, nos familiarizamos con el procesador y obtenemos un código de depuración conveniente que es útil al escribir un emulador de CPU.

Aquí está el algoritmo de desmontaje del código 8080:

  1. Lee el código en el búfer
  2. Obtenemos un puntero al comienzo del búfer
  3. Use el byte en el puntero para determinar el código de operación.
  4. Muestra el nombre del código de operación, si es necesario, usando bytes después del código de operación como datos
  5. Mueva el puntero al número de bytes utilizados por este comando (1, 2 o 3 bytes)
  6. Si el búfer no termina, vaya al paso 3

Para sentar las bases del procedimiento, agregué un par de instrucciones a continuación. Presentaré el procedimiento completo para la descarga, pero le recomiendo que intente escribirlo usted mismo. No tomará mucho tiempo y, en paralelo, aprenderá el conjunto de instrucciones del procesador 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; } 

En el proceso de escribir este procedimiento y estudiar cada código de operación, aprendí mucho sobre el procesador 8080.

  1. Me di cuenta de que la mayoría de los equipos toman un byte, el resto dos o tres. El código anterior supone que el comando tiene un tamaño de un byte, pero las instrucciones de dos y tres bytes cambian el valor de la variable "opbytes" para devolver el tamaño correcto del comando.
  2. El 8080 tiene registros con los nombres A, B, C, D, E, H y L. También hay un contador de programas (contador de programas, PC) y un puntero de pila separado (puntero de pila, SP).
  3. Algunas instrucciones funcionan con registros en pares: B y C son un par, así como DE y HL.
  4. A es un registro especial, muchas instrucciones funcionan con él.
  5. HL también es un registro especial, se utiliza como una dirección para cada lectura y escritura de datos en la memoria.
  6. Sentí curiosidad por el equipo "RST", así que leí un poco la guía. Noté que ejecuta el código en lugares fijos y la referencia menciona el manejo de interrupciones. Tras una lectura adicional, resultó que todo este código al comienzo de la ROM era interrumpir las rutinas de servicio (ISR). Las interrupciones pueden generarse mediante programación utilizando el comando RST, o generadas por fuentes de terceros (no el procesador 8080).

Para convertir todo esto en un programa de trabajo, acabo de preparar un procedimiento que realiza los siguientes pasos:

  1. Abre un archivo lleno de código compilado 8080
  2. Lo lee en el búfer de memoria
  3. Pasa a través del búfer de memoria, causando Disassemble8080Op
  4. Aumenta la PC devuelta por Disassemble8080Op
  5. Salidas al final del búfer.

Puede verse más o menos así:

  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; } 

En la segunda parte, examinaremos el resultado obtenido al desmontar los ROM Space Invaders.

Asignación de memoria


Antes de comenzar a escribir un emulador de procesador, necesitamos estudiar otro aspecto. Todas las CPU tienen la capacidad de comunicarse con un cierto número de direcciones. Los procesadores más antiguos tenían direcciones de 16, 24 o 32 bits. El 8080 tiene 16 contactos de dirección, por lo que las direcciones están en el rango de 0- $ FFFF.

Para comprender la asignación de memoria del juego, necesitamos realizar una pequeña investigación. Después de recopilar la información aquí y aquí , descubrí que la ROM se encuentra en la dirección 0, y el juego tiene 8 KB de RAM a partir de $ 2000.

El autor de una de las páginas descubrió que el búfer de video comienza en la RAM con una dirección de $ 2,400, y también nos dijo cómo se usan los puertos de E / S 8080 para comunicarse con los controles y el equipo de audio. Genial

Dentro del archivo ROM invaders.zip, que se puede encontrar en Internet, hay cuatro archivos: invaders.e, .f, .g y .h. Después de buscar en Google, me encontré con un artículo informativo que dice cómo guardar estos archivos en la memoria:

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-:


Todavía hay información útil, pero aún no estamos listos para usarla.

Detalles sangrientos


Si desea saber qué tamaño del espacio de direcciones tiene el procesador, puede comprender esto al observar sus características. La especificación 8080 nos dice que el procesador tiene 16 contactos de dirección, es decir, usa direccionamiento de 16 bits. (En lugar de las especificaciones, es suficiente leer el manual, Wikipedia, google, etc.)

En Internet hay mucha información sobre el hardware de Space Invaders. Si no pudo encontrar esta información, puede obtenerla de dos maneras:

  • Observe el código que se ejecuta en el emulador y descubra lo que hace. Toma notas y observa con cuidado. Debe ser lo suficientemente simple como para comprender, por ejemplo, dónde, en opinión del juego, se debe ubicar la RAM. También es fácil determinar el lugar donde está buscando memoria de video (pasaremos un tiempo estudiando esto).
  • Encuentre el diagrama de circuito de la máquina arcade y rastree las señales de los contactos de dirección de la CPU. Mira a dónde van. Por ejemplo, A15 (dirección más antigua) solo puede ir a ROM. De esto podemos concluir que las direcciones de la ROM comienzan en $ 8000.

Puede ser muy interesante e informativo descubrirlo usted mismo observando la ejecución del código. Alguien tuvo que lidiar con todo esto por primera vez.

Desarrollo de línea de comando


El objetivo de este tutorial no es enseñarle a escribir código para una plataforma específica, aunque no podremos evitar el código específico de la plataforma. Espero que antes del inicio del proyecto, ya supiera cómo compilar para su plataforma de destino.

Cuando trabaja con código independiente, que simplemente lee archivos y muestra texto en la consola, no es necesario utilizar un sistema de desarrollo demasiado complicado. De hecho, solo complica las cosas. Todo lo que necesitas es un editor de texto y terminal.

Creo que cualquiera que quiera programar en un nivel bajo debería saber cómo crear programas simples desde la línea de comandos. Puede considerar que lo molesto, pero sus habilidades de hacker de élite no valen mucho si no puede funcionar fuera de Visual Studio.

En Mac, puede usar TextEdit y Terminal para compilar. En Linux, puede usar gedit y Konsole. En Windows, puede instalar cygwin y herramientas, y luego usar N ++ u otro editor de texto. Si quieres ser realmente genial, entonces todas estas plataformas admiten vi y emacs para la edición de texto.

Compilar programas de un solo archivo usando la línea de comandos es una tarea trivial. Supongamos que guardó su programa en un archivo llamado 8080dis.c . Vaya a la carpeta con este archivo de texto y cc 8080dis.c así: cc 8080dis.c . Si no especifica el nombre del archivo de salida, se llamará a.out y puede ejecutarlo escribiendo ./a.out .

Eso, de hecho, es todo.

Usando un depurador


Si está trabajando en uno de los sistemas basados ​​en Unix, aquí hay una breve introducción a la depuración de programas de línea de comandos utilizando GDB. Necesita compilar el programa así: cc -g -O0 8080dis.c . La -g genera información de depuración (es decir, puede realizar la depuración en función del texto fuente), y la -O0 deshabilita las optimizaciones para que cuando pase por el programa, el depurador pueda rastrear con precisión el código de acuerdo con el texto fuente.

Aquí está el registro anotado del inicio de una sesión de depuración. Mis comentarios están en líneas marcadas con un signo de número (#).

  $ 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"); 

Desensamblador, Parte 2


Ejecute el desensamblador para el archivo ROM invaders.h y observe la información que se muestra.

  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 */ 

Las primeras instrucciones corresponden a las que escribimos manualmente anteriormente. Después de ellos hay varias instrucciones nuevas. A continuación he insertado datos hexadecimales para referencia. Tenga en cuenta que si compara la memoria con los comandos, las direcciones son como si estuvieran almacenadas en la memoria en el orden inverso. Así es Esto se llama little endian: las máquinas con little endian, como el 8080, almacenan primero los bytes menos significativos de números. (Más sobre endian se describe a continuación).

Mencioné anteriormente que este código es el código ISR para el juego Space Invaders. El código para las interrupciones 0, 1, 2, ... 7 comienza con la dirección $ 0, $ 8, $ 20, ... $ 38. Parece que el 8080 solo da 8 bytes para cada ISR. A veces, el programa Space Invaders pasa por alto este sistema simplemente moviéndose a otra dirección con más espacio. (Esto sucede a $ 000c).

Además, ISR 2 parece ser más largo que la memoria asignada para ello. Su código va a $ 0018 (este es el lugar para ISR 3). Creo que los Space Invaders no esperan ver nada que use la interrupción 3.

El archivo ROM de Space Invaders de Internet consta de cuatro partes. Explicaré esto a continuación, pero por ahora, para pasar a la siguiente sección, necesitamos fusionar estos cuatro archivos en uno. En Unix:

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

Ahora ejecute el desensamblador con el archivo resultante de "invasores". Cuando un programa comienza en $ 0000, lo primero que hace es cambiar a $ 18d4. Consideraré esto como el comienzo del programa. Echemos un vistazo rápido a este código.

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

Entonces, realiza dos operaciones y llama $ 01e6. Voy a insertar parte del código con transiciones en este 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 en la asignación de memoria de Space Invaders, algunas de estas direcciones son interesantes. $ 2000 es el comienzo de un programa de "RAM en funcionamiento". $ 2,400 es el comienzo de la memoria de video.

Agreguemos comentarios al código para explicar lo que hace directamente al inicio:

  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 este código copiará 256 bytes de $ 1b00 a $ 2000. Por qué No lo se Puede estudiar el programa con más detalle y reflexionar sobre lo que hace.

Hay un problema aquí. Si tenemos un fragmento de memoria arbitrario que contiene código, entonces los datos probablemente se alternarán con él.

Por ejemplo, los sprites para los personajes del juego pueden mezclarse con el código. Cuando un desensamblador cae en tal fragmento de memoria, pensará que se trata de código y continuará "masticando". Si no tiene suerte, cualquier código desensamblado después de este dato puede ser incorrecto.

Si bien casi no podemos hacer nada al respecto. Solo tenga en cuenta que tal problema existe. Si ves algo como esto:

  • transición de un código exactamente bueno a un equipo que no está en la lista de desensambladores
  • flujo de código sin sentido (por ejemplo, POP B POP B POP B POP C XTHL XTHL XTHL)

Aquí, probablemente, hay datos que arruinaron parte del código desmontado. Si esto sucede, debe comenzar de nuevo desde el desplazamiento.

Resulta que los invasores del espacio periódicamente se encuentran con ceros. Si nuestro desmontaje alguna vez se detiene, los ceros lo obligarán a realizar un reinicio.

Un análisis detallado del código de Space Invaders se puede encontrar aquí .

Endian


Los bytes se almacenan de manera diferente en diferentes modelos de procesador, y el almacenamiento depende del tamaño de los datos. Las máquinas big-endian almacenan datos de mayor a menor. Little-endian los mantiene desde los más pequeños hasta los más viejos. Si se escribe un entero 0xAABBCCDD de 32 bits en la memoria de cada máquina, se verá así:

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

Big-endian: $ AA $ BB $ CC $ DD

Comencé a programar en procesadores Motorola que usaban big-endian, así que me pareció más "natural", pero luego también me acostumbré a little-endian.

Mi desensamblador y mi emulador evitan completamente el problema endian porque solo leen un byte a la vez. Si desea, por ejemplo, utilizar un lector de 16 bits para leer la dirección desde la ROM, tenga en cuenta que este código no es portátil entre arquitecturas de CPU.

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


All Articles