Creaci贸n de una m谩quina arcade emulador. Parte 1

imagen

Escribir un emulador de m谩quinas recreativas es un gran proyecto educativo, y en este tutorial analizaremos muy detalladamente todo el proceso de desarrollo. 驴Realmente quieres poner tus manos en el procesador? Entonces crear un emulador es la mejor manera de aprenderlo.

Necesitar谩 conocimiento de C, as铆 como conocimiento de ensamblador. Si no conoce el lenguaje ensamblador, escribir un emulador es la mejor manera de aprenderlo. Tambi茅n necesitar谩 dominar las matem谩ticas hexadecimales (tambi茅n conocido como base 16 o simplemente "hexadecimal"). Hablar茅 sobre este tema.

Decid铆 elegir un emulador para la m谩quina Space Invaders, que usa el procesador 8080. Este juego y este procesador son muy populares, porque en Internet puedes encontrar mucha informaci贸n sobre ellos. Lo necesitar谩 para completar el proyecto.

Todo el c贸digo fuente del tutorial se carga en github . Si no ha dominado el trabajo con git, en la p谩gina de github hay un bot贸n "Descargar ZIP" que le permite descargar el archivo con todo el c贸digo.

Introducci贸n a los n煤meros binarios y hexadecimales.


En matem谩ticas "ordinarias", se utiliza el sistema de n煤meros decimales. Cada d铆gito del n煤mero puede tener un valor de cero a nueve, y cuando superamos el 9, agregamos uno al n煤mero en el siguiente d铆gito y comenzamos nuevamente desde cero. Todo esto es bastante simple y directo, y probablemente nunca lo pensaste.

Es posible que haya sabido o escuchado que las computadoras funcionan con datos binarios. Los geeks inform谩ticos llaman matem谩tica decimal base-10 y la llamada binaria base-2. En notaci贸n binaria, cada d铆gito de un n煤mero puede tener solo dos valores, cero o uno. En el c贸digo binario, el recuento es el siguiente: 0, 1, 10, 11, 100, 101, 110, 111, 1000. Estos no son n煤meros decimales, por lo que no puede llamarlos "cero, uno, diez, once, cien, ciento uno". Se pronuncian como "cero, uno, uno-cero, uno-uno, uno-cero-cero", etc. Raramente leo n煤meros binarios en voz alta, pero si es necesario, debe indicar claramente el sistema de n煤meros utilizado. Diez, once y cien no tienen significado en notaci贸n binaria.

En notaci贸n decimal, un n煤mero tiene los siguientes d铆gitos: unidades, decenas, cientos, miles, decenas de miles, etc. En el sistema binario, los siguientes d铆gitos: unidades, deuces, fours, ochos, etc. En inform谩tica, el valor de cada bit binario se llama bit. 8 bits forman un byte.

En t茅rminos binarios, una cadena de n煤meros r谩pidamente se vuelve muy larga. Para representar el n煤mero decimal 20,000 en t茅rminos binarios, se requieren 16 d铆gitos: 0b100111000100000. Para solucionar este problema, es conveniente usar un sistema de n煤meros hexadecimales, tambi茅n conocido como base-16 (o hexadecimal). En base-16, cada d铆gito contiene 16 valores. Para valores de cero a nueve, se usan los mismos caracteres que en la base 10, pero para los 6 valores restantes, las sustituciones se usan en forma de las primeras 6 letras del alfabeto, de A a F.

La cuenta en el sistema hexadecimal se realiza de la siguiente manera: 0 1 2 3 4 5 6 7 8 9 ABCDEF 10 11 12, etc. En hexadecimal, decenas, cientos, etc. no tienen el mismo significado que en decimal, por lo que las personas pronuncian los n煤meros por separado. Por ejemplo, $ A57 se pronuncia en voz alta como "A-cinco-siete". Para mayor claridad, tambi茅n puede agregar hex谩gono, por ejemplo, "A-cinco-siete-hex". En el sistema de n煤meros hexadecimales, el equivalente del n煤mero decimal 20,000 es $ 4E20, una forma mucho m谩s compacta en comparaci贸n con los 16 bits del sistema binario.

Creo que el sistema hexadecimal fue elegido debido a una conversi贸n muy natural de binario a hexadecimal y viceversa. Cada d铆gito hexadecimal corresponde a 4 bits (4 bits) de un n煤mero binario similar. 2 d铆gitos hexadecimales forman un byte (8 bits). Un solo d铆gito hexadecimal puede llamarse nibble, y algunas personas incluso lo escriben a trav茅s de y como "nybble".

Cada d铆gito hexadecimal tiene 4 d铆gitos binarios.
MaleficioUn5 57 7
Binario101001010111

Al escribir el c贸digo C, se cree que el n煤mero es decimal (base-10), a menos que se marque lo contrario. Para decirle al compilador de C que el n煤mero es binario, agregamos el n煤mero cero y la letra b en min煤scula, de esta manera: 0b1101101 . El n煤mero hexadecimal se puede escribir en c贸digo C agregando al principio de cero yx en min煤sculas: 0xA57 . Algunos lenguajes de ensamblaje usan el signo de d贸lar $: $A57 para indicar un n煤mero hexadecimal.

Si lo piensa, la conexi贸n entre n煤meros binarios, hexadecimales y decimales es bastante obvia, pero para el primer ingeniero, que hab铆a pensado en esto antes de la invenci贸n de la computadora, esto deber铆a haberse convertido en un momento de comprensi贸n.

Entendido todo esto? Genial

Una breve introducci贸n al procesador.


Si ya lo sabe, puede saltarse la secci贸n de forma segura.

Una unidad central de procesamiento (CPU) es una m谩quina dise帽ada para ejecutar programas. Los bloques fundamentales de la CPU son registros e instrucciones. Como desarrollador de software, puede tratar estos registros como variables. En nuestro procesador 8080, entre otros registros, hay registros de 8 bits llamados A, B, C, D y E. Estos registros se pueden interpretar como el siguiente c贸digo C:

 unsigned char A, B, C, D, E; 

Todos los procesadores tambi茅n tienen un contador de programas (Contador de programas, PC). Puedes tomarlo como un puntero.

 unsigned char* pc; 

Para una CPU, un programa es una secuencia de n煤meros hexadecimales. Cada instrucci贸n de lenguaje ensamblador en 8080 corresponde a 1-3 bytes en el programa. Para saber qu茅 comando corresponde a qu茅 n煤mero, es 煤til el manual del procesador (o cualquier otra informaci贸n sobre el procesador 8080 de Internet).

Los nombres de los comandos (instrucciones) a menudo son mnemot茅cnicos de las operaciones realizadas por estos comandos. El mnem贸nico para cargar en 8080 es MOV (mover), y ADD se usa para realizar la adici贸n.

Ejemplos


El valor de memoria actual indicado por el contador de instrucciones es 0x79. Esto cumple con la instrucci贸n MOV A,C procesador 8080. Este c贸digo de ensamblaje en c贸digo C se parece a A=C; .

Si, en cambio, el valor en la PC ser铆a 0x80, entonces el procesador ejecutar铆a ADD B En C, esto corresponde a la cadena A = A + B; .

Puede encontrar una lista completa de las instrucciones del procesador 8080 aqu铆 . Para implementar nuestro emulador, utilizaremos esta informaci贸n.

Tiempos


En la CPU, la ejecuci贸n de cada instrucci贸n requiere una cierta cantidad de tiempo (sincronizaci贸n), medida en ciclos. En los procesadores modernos, esta informaci贸n puede ser dif铆cil de obtener, porque los tiempos dependen de muchos aspectos diferentes. Pero en procesadores m谩s antiguos como el 8080, los tiempos son constantes y el fabricante del procesador suele proporcionar esta informaci贸n. Por ejemplo, una instrucci贸n de transferencia de registro a registro MOV toma 1 ciclo.

La informaci贸n de tiempo es 煤til para escribir c贸digo eficiente en el procesador. Un programador puede tratar de evitar las instrucciones que tardan muchos ciclos en completarse.

M谩s importante para nosotros es que usaremos informaci贸n de tiempo para emular el procesador. Para que el juego funcione de la misma manera que en el original, las instrucciones deben ejecutarse a la velocidad correcta. Algunos emuladores ponen mucho esfuerzo en esto, pero cuando lleguemos a esto, tendremos que decidir qu茅 precisi贸n queremos obtener.

Operaciones l贸gicas


Antes de cerrar el tema de los n煤meros binarios y hexadecimales, deber铆amos hablar sobre operaciones l贸gicas. Probablemente ya est茅 acostumbrado a usar la l贸gica en su c贸digo, por ejemplo, en construcciones como if ((conditionA) and (conditionB)) . En los programas que funcionan directamente con hardware, a menudo tiene que manipular bits individuales de n煤meros.

Y operaci贸n


Aqu铆 est谩n todos los resultados posibles de la operaci贸n AND (AND) (tabla de verdad) entre dos n煤meros de un solo bit.

xyResultado
0 00 00 0
0 010 0
10 00 0
111

El resultado de AND es igual a la unidad solo cuando ambos valores son iguales a la unidad. Cuando combinamos dos n煤meros con la operaci贸n AND, AND para cada bit de un n煤mero es AND con el bit correspondiente del otro n煤mero. El resultado se almacena en este bit del n煤mero de destino. Probablemente sea mejor solo mirar un ejemplo:

binariomaleficio
fuente x0 0110 010 011$ 6B
fuente y110 010 00 010 0$ D2
x e y0 010 00 00 00 010 0$ 42

En C, la operaci贸n AND l贸gica es un simple signo "&".

Operaci贸n OR (OR)


La operaci贸n OR funciona de manera similar. La 煤nica diferencia es que el resultado ser谩 igual a uno si al menos uno de los valores de x o y es igual a uno.

xyResultado
0 00 00 0
0 011
10 01
111

binariomaleficio
fuente x0 0110 010 011$ 6B
fuente y110 010 00 010 0$ D2
x O y111110 011$ Fb

En C, una operaci贸n OR l贸gica se indica mediante una barra vertical "|".

驴Por qu茅 es esto importante?


En muchos procesadores m谩s antiguos, y especialmente en m谩quinas recreativas, el juego a menudo requiere trabajar con solo una parte del n煤mero. A menudo hay un c贸digo similar:

  /*  1:     */ char *buttons_ptr = (char *)0x2043; char buttons = *buttons_ptr; if (buttons & 0x4) HandleLeftButton(); /*  2:  LED-    */ char * LED_pointer = (char *) 0x2089; char led = *LED_pointer; led = led | 0x40; //,  LED   6 *LED_pointer = led; /*  3:   LED- */ char * LED_pointer = (char *) 0x2089; char led = *LED_pointer; led = led & 0xBF; //  6 *LED_pointer = led; 

En el ejemplo 1, la direcci贸n $ 2043 asignada en la memoria es la direcci贸n de los botones en el panel de control. Este c贸digo lee y responde al bot贸n presionado. (隆Por supuesto, en Space Invaders este c贸digo estar谩 en lenguaje ensamblador!)

En el ejemplo 2, el juego quiere encender un indicador LED, que se encuentra en el bit 6 de la direcci贸n de $ 2089 asignada en la memoria. El c贸digo debe leer el valor existente, cambiar solo un bit y volver a escribirlo.

En el ejemplo 3, debe apagar el indicador del ejemplo 2, por lo que el c贸digo debe restablecer el bit 6 de la direcci贸n $ 2089. Esto se puede hacer realizando la operaci贸n AND para el byte de control del indicador con un valor para el que solo el bit 6 es cero. Por lo tanto, afectaremos solo 6, dejando los bits restantes sin cambios.

Esto generalmente se llama una "m谩scara". En C, una m谩scara generalmente se escribe usando el operador NOT, denotado por una tilde ("~"). Por lo tanto, en lugar de escribir 0xBF , simplemente escribo ~0x40 y obtengo el mismo n煤mero, pero sin poner mucho esfuerzo.

Introducci贸n al lenguaje ensamblador


Si lee este tutorial, probablemente est茅 familiarizado con la programaci贸n de computadoras, por ejemplo, en Java o Python. Estos lenguajes le permiten hacer mucho trabajo en solo unas pocas l铆neas de c贸digo. El c贸digo se considera h谩bilmente escrito si hace el mayor trabajo posible en la menor cantidad de l铆neas posible, posiblemente incluso utilizando la funcionalidad de las bibliotecas integradas. Tales lenguajes se llaman "lenguajes de alto nivel".

En lenguaje ensamblador, por el contrario, no hay funciones integradas para salvar vidas, y se pueden requerir muchas l铆neas de c贸digo simples para completar tareas simples. El lenguaje ensamblador se considera un lenguaje de bajo nivel. En 茅l, debe acostumbrarse a pensar al estilo de "驴qu茅 secuencia espec铆fica de pasos se deben tomar para completar esta tarea?"

Lo m谩s importante que necesita saber sobre el lenguaje ensamblador es que cada l铆nea se traduce en un comando de procesador.

Considere tal construcci贸n del lenguaje C:

 int a = b + 100; 

En lenguaje ensamblador, esta tarea deber谩 realizarse en la siguiente secuencia:

  1. Cargue la direcci贸n de la variable B en el registro 1
  2. Cargue el contenido de esta direcci贸n de memoria en el registro 2
  3. Agregue valor directo 0x64 para registrar 2
  4. Cargue la direcci贸n de la variable A en el registro 1
  5. Escriba el contenido del registro 2 en la direcci贸n almacenada en el registro 1

En c贸digo, se ver谩 m谩s o menos as铆:

  lea a1, #$1000 ;   a lea a2, #$1008 ;   b move.l d0,(a2) add.l d0, #$64 mov (a1),d0 

Vale la pena se帽alar lo siguiente:

  • En un lenguaje de alto nivel, el compilador decide d贸nde colocar las variables en la memoria. Al escribir c贸digo en ensamblador, usted mismo es responsable de cada direcci贸n de memoria que utilizar谩.
  • En la mayor铆a de los lenguajes ensambladores, los corchetes significan "memoria en esta direcci贸n".
  • En la mayor铆a de los lenguajes ensambladores, # denota un n煤mero algebraico, tambi茅n llamado valor inmediato. Por ejemplo, en la l铆nea 1 del ejemplo anterior, el c贸digo realmente escribe el valor # 0x1000 para registrar a1. Si el c贸digo se parece a move.l a1, ($1000) , entonces a1 recibir铆a el contenido de la memoria en la direcci贸n 0x1000.
  • Cada procesador tiene su propio lenguaje ensamblador, y la transferencia de c贸digo de un procesador a otro puede ser dif铆cil.
  • Este no es un lenguaje ensamblador de procesador real, se me ocurri贸 como ejemplo.

Sin embargo, hay una cosa en com煤n entre los programadores inteligentes de alto nivel y los asistentes de ensamblador. Los programadores de ensambladores consideran un honor completar la tarea de la manera m谩s eficiente posible y minimizar la cantidad de instrucciones utilizadas. El c贸digo para las m谩quinas recreativas generalmente est谩 altamente optimizado y todos los jugos se exprimen de cada byte y ciclo extra.

Pilas


Hablemos un poco m谩s sobre el lenguaje ensamblador. En cualquier programa de computadora bastante complejo en el ensamblador se utilizan subrutinas. La mayor铆a de las CPU tienen una estructura llamada pila.

Imagina una pila en forma de pila. Si necesitamos guardar un n煤mero, lo colocamos en la parte superior de la pila. Cuando necesitamos traerlo de vuelta, lo tomamos de la parte superior de la pila. Los programadores de ensambladores llaman "empujar" al n煤mero emergente en la pila, y al llamarlo emergente se llama "pop".

Digamos que mi programa necesita llamar a una subrutina. Puedo escribir un c贸digo similar:

  0x1000 move.l (sp), d0 ;  d0   0x1004 add.l sp, #4 ;     0x1008 move.l (sp), d1 ;  d1   0x1010 add.l sp, #4 ;  .. 0x1014 move.l (sp), a0 0x1018 add.l sp, #4 0x101C move.l (sp), a1 0x1020 add.l sp, #4 0x1024 move.l (sp), #0x1030 ;   0x1028 add.l sp, #4 0x102C jmp #0x2040 ;   - 0x2040 0x1030 move.l a1, (sp) ;    0x1034 sub.l sp, #4 ;    0x1038 move.l a0, (sp) ;    0x103c sub.l sp, #4  .. 

El c贸digo que se muestra arriba empuja los valores d0, d1, a0 y a1 a la pila. La mayor铆a de los procesadores usan un puntero de pila. Este puede ser un registro regular, por convenci贸n utilizado como un puntero de pila, o un registro especial con funciones para ciertas instrucciones.

En los procesadores de la serie 68K, el puntero de la pila solo se determina por convenci贸n; de lo contrario, es un registro regular. En nuestro procesador 8080, el registro SP es un registro especial. Tiene comandos PUSH y POP que escriben y salen de la pila en un solo comando.

En nuestro proyecto de emulador, no escribiremos c贸digo desde cero. Pero si necesita analizar programas en lenguaje ensamblador, entonces es bueno aprender a reconocer tales construcciones.

Idiomas de alto nivel


Al escribir un programa en un lenguaje de alto nivel, todas las operaciones de guardar y restaurar registros se realizan con cada llamada de funci贸n. No pensamos en ellos, porque el compilador los trata. Las llamadas a funciones en un lenguaje de alto nivel pueden ocupar mucha memoria y tiempo de procesador.

驴Alguna vez ha experimentado un bloqueo de un programa al llamar a una subrutina en un bucle infinito? Esto puede suceder porque cada llamada a la funci贸n introdujo valores de registro en la pila, y en alg煤n momento la memoria se agot贸. (Si la pila crece demasiado, esto se llama desbordamiento de pila o desbordamiento de pila).

Es posible que haya o铆do hablar de las funciones en l铆nea. Evitan guardar y restaurar registros al incluir el c贸digo de rutina en la funci贸n de llamada. El c贸digo se hace m谩s grande, pero gracias a esto, se guardan varios comandos y operaciones de lectura / escritura en la memoria.

Convenciones de llamadas


Al escribir un programa ensamblador que solo llame a su c贸digo, puede decidir por s铆 mismo c贸mo se comunicar谩n las rutinas entre s铆. Por ejemplo, 驴c贸mo vuelvo a la funci贸n de llamada una vez que se completa la rutina? Una forma es escribir la direcci贸n del remitente en un registro espec铆fico. El otro es colocar la direcci贸n de retorno en la parte superior de la pila. Muy a menudo, la decisi贸n depende de lo que admita el procesador. El 8080 tiene un comando CALL que empuja la direcci贸n de retorno de una funci贸n a la pila. Quiz谩s utilizar谩 este comando 8080 para implementar llamadas de subrutina.

Se debe tomar una decisi贸n m谩s. 驴La preservaci贸n del registro es responsabilidad de la funci贸n o subrutina que llama? En el ejemplo anterior, la funci贸n de llamada almacena los registros. Pero, 驴y si tenemos 32 registros? Guardar y restaurar 32 registros cuando una rutina usa solo una peque帽a fracci贸n de ellos ser谩 una p茅rdida de tiempo.

La compensaci贸n puede ser un enfoque mixto. Supongamos que elegimos una pol铆tica en la que una rutina puede usar los registros r10-r32 sin guardar su contenido, pero no puede destruir r1-r9. En una situaci贸n similar, la funci贸n de llamada sabe lo siguiente:

  • Al regresar de una funci贸n, el contenido de r1-r9 permanecer谩 sin cambios.
  • No puedo depender del contenido de r10-r32
  • Si necesito un valor en r10-r32 despu茅s de llamar a una subrutina, entonces antes de llamarlo necesito guardarlo en alg煤n lugar

Del mismo modo, cada rutina sabe lo siguiente:

  • Puedo destruir r10-r32
  • Si quiero usar r1-r9, entonces necesito guardar el contenido y restaurarlo antes de volver a la funci贸n que me llam贸

Abi


En la mayor铆a de las plataformas modernas, dichas pol铆ticas son creadas por ingenieros y publicadas en documentos llamados ABI (Application Binary Interface). Gracias a este documento, los creadores del compilador saben c贸mo compilar c贸digo que puede llamar al c贸digo compilado por otros compiladores. Si desea escribir c贸digo de ensamblador que pueda funcionar en dicho entorno, entonces necesita conocer ABI y escribir c贸digo de acuerdo con 茅l.

Conocer ABI tambi茅n ayuda a depurar c贸digo cuando no tiene acceso a la fuente. El ABI define la ubicaci贸n de los par谩metros para las funciones, por lo que al considerar cualquier subprograma, puede examinar estas direcciones para comprender qu茅 se pasa a las funciones.

De vuelta al emulador


La mayor铆a del c贸digo de ensamblaje escrito a mano, especialmente para los procesadores y juegos arcade m谩s antiguos, no sigue ABI. Los programas est谩n ensamblados y pueden no tener muchas rutinas. Cada rutina guarda y restaura registros solo en caso de emergencia.

Si desea comprender lo que hace el programa, ser铆a bueno comenzar marcando las direcciones que est谩n destinadas a los comandos CALL.

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


All Articles