Création d'une machine d'arcade d'émulation. 2e partie

image

La première partie est ici .

Démonteur du processeur 8080


Connaissance


Nous aurons besoin d'informations sur les opcodes et leurs commandes respectives. Lorsque vous recherchez des informations sur Internet, vous remarquerez qu'il y a beaucoup d'informations mitigées sur le 8080 et le Z80. Le Z80 était un adepte du 8080 - il exécute toutes les instructions du 8080 avec les mêmes codes hexadécimaux, mais a également des instructions supplémentaires. Je pense, alors que vous devez éviter les informations sur le Z80, afin de ne pas vous tromper. J'ai créé une table d'opcode pour notre travail, elle est ici .

Chaque processeur possède un guide de référence écrit par le fabricant. Habituellement, cela s'appelle quelque chose comme "Manuel de l'environnement du programmeur". Le manuel 8080 est appelé le Manuel de l'utilisateur des systèmes de micro-ordinateurs Intel 8080. Il a toujours été appelé «livre de données», je l'appellerai donc également ainsi. J'ai pu télécharger la référence 8080 sur http://www.datasheetarchive.com/ . Ce PDF est une numérisation de faible qualité, donc si vous trouvez une meilleure version, utilisez-la.

Commençons et jetons un œil à la ROM Space Invaders. (Le fichier ROM peut être trouvé sur Internet.) Je travaille sur Mac OS X, donc j'utilise simplement la commande hexdump pour afficher son contenu. Pour continuer à travailler, trouvez l'éditeur hexadécimal pour votre plateforme. Voici les 128 premiers octets du fichier 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 ... 

C'est le début du programme Space Invaders. Chaque nombre hexadécimal est une commande ou des données pour le programme. Nous pouvons utiliser une référence ou d'autres informations de référence pour comprendre ce que signifient ces codes hexadécimaux. Explorons un peu plus le code image ROM.

Le premier octet de ce programme est 00 $. En regardant le tableau, nous voyons qu'il s'agit de NOP, ainsi que des deux commandes suivantes. (Mais ne vous découragez pas, Space Invaders a probablement utilisé ces commandes comme un délai pour laisser le système se calmer un peu après la mise sous tension.)

La quatrième commande est $ C3, c'est-à-dire, à en juger par la table, c'est JMP. La définition d'une commande JMP indique qu'elle reçoit une adresse à deux octets, c'est-à-dire que les deux octets suivants sont l'adresse de saut JMP. Ensuite, deux NOP supplémentaires viennent ... alors, vous savez quoi? Permettez-moi de signer moi-même les premières instructions ...

  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 

Il semble qu'il y ait un moyen d'automatiser ce processus ...

Démonteur, partie 1


Un désassembleur est un programme qui traduit simplement un flux de nombres hexadécimaux en code source en langage assembleur. C'est exactement la tâche que nous avons effectuée à la main dans la section précédente - une excellente occasion d'automatiser ce travail. En écrivant ce morceau de code, nous nous familiarisons avec le processeur et obtenons un morceau de code de débogage pratique, qui est utile lors de l'écriture d'un émulateur de CPU.

Voici l'algorithme de démontage du code 8080:

  1. Lire le code dans le tampon
  2. Nous obtenons un pointeur vers le début du tampon
  3. Utilisez l'octet du pointeur pour déterminer l'opcode.
  4. Afficher le nom de l'opcode, si nécessaire en utilisant des octets après l'opcode comme données
  5. Déplacez le pointeur sur le nombre d'octets utilisés par cette commande (1, 2 ou 3 octets)
  6. Si le tampon ne se termine pas, passez à l'étape 3

Pour jeter les bases de la procédure, j'ai ajouté quelques instructions ci-dessous. Je vais exposer la procédure complète de téléchargement, mais je vous recommande d'essayer de l'écrire vous-même. Cela ne prendra pas beaucoup de temps, et en parallèle, vous apprendrez le jeu d'instructions du processeur 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 écrivant cette procédure et en étudiant chaque opcode, j'ai beaucoup appris sur le processeur 8080.

  1. J'ai réalisé que la plupart des équipes prenaient un octet, les deux ou trois restants. Le code ci-dessus suppose que la commande a une taille d'un octet, mais les instructions à deux et trois octets modifient la valeur de la variable «opbytes» pour renvoyer la taille correcte de la commande.
  2. Le 8080 a des registres avec les noms A, B, C, D, E, H et L. Il y a aussi un compteur de programme (compteur de programme, PC) et un pointeur de pile séparé (pointeur de pile, SP).
  3. Certaines instructions fonctionnent avec des registres par paires: B et C sont une paire, ainsi que DE et HL.
  4. A est un registre spécial, de nombreuses instructions fonctionnent avec.
  5. HL est également un registre spécial, il est utilisé comme adresse pour chaque lecture et écriture de données en mémoire.
  6. Je suis devenu curieux au sujet de l'équipe «RST», alors j'ai lu un peu le guide. J'ai remarqué qu'il exécute le code à des endroits fixes et la référence mentionne la gestion des interruptions. Après lecture, il s'est avéré que tout ce code au début de la ROM était des routines de service d'interruption (ISR). Les interruptions peuvent être générées par programme à l'aide de la commande RST ou générées par des sources tierces (pas le processeur 8080).

Pour transformer tout cela en programme de travail, je viens de préparer une procédure qui effectue les étapes suivantes:

  1. Il ouvre un fichier rempli de code compilé 8080
  2. Le lit dans la mémoire tampon
  3. Passe à travers la mémoire tampon, provoquant le démontage8080Op
  4. Augmente le PC renvoyé par Disassemble8080Op
  5. Quitte à la fin du tampon

Cela pourrait ressembler à ceci:

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

Dans la deuxième partie, nous examinerons la sortie obtenue en démontant les ROM Space Invaders.

Allocation de mémoire


Avant de commencer à écrire un émulateur de processeur, nous devons étudier un autre aspect. Tous les CPU ont la capacité de communiquer avec un certain nombre d'adresses. Les processeurs plus anciens avaient des adresses 16, 24 ou 32 bits. Le 8080 a 16 contacts d'adresse, donc les adresses sont comprises entre 0 et $ FFFF.

Pour comprendre l'allocation de mémoire du jeu, nous devons mener une petite enquête. Après avoir collecté les informations ici et ici , j'ai découvert que la ROM se trouve à l'adresse 0 et que le jeu a 8 Ko de RAM à partir de 2000 $.

L'auteur d'une des pages a découvert que le tampon vidéo commence en RAM avec une adresse de 2400 $ et nous a également expliqué comment les ports d'entrée-sortie 8080 sont utilisés pour communiquer avec les commandes et l'équipement audio. Super!

Le fichier ROM invaders.zip, qui se trouve sur Internet, contient quatre fichiers: invaders.e, .f, .g et .h. Après avoir googlé, je suis tombé sur un article informatif qui explique comment mettre ces fichiers en mémoire:

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


Il existe encore quelques informations utiles, mais nous ne sommes pas encore prêts à les utiliser.

Détails sanglants


Si vous voulez connaître la taille de l'espace d'adressage du processeur, vous pouvez le comprendre en examinant ses caractéristiques. La spécification 8080 nous indique que le processeur possède 16 contacts d'adresse, c'est-à-dire qu'il utilise un adressage 16 bits. (Au lieu de spécifications, il suffit de lire le manuel, Wikipedia, google, etc.)

Sur Internet, il y a beaucoup d'informations sur le matériel de Space Invaders. Si vous n'avez pas pu trouver ces informations, vous pouvez les obtenir de plusieurs manières:

  • Regardez le code s'exécuter dans l'émulateur et voyez ce qu'il fait. Prenez des notes et regardez attentivement. Il doit être assez simple pour comprendre, par exemple, où, de l'avis du jeu, la RAM doit être située. Il est également facile de déterminer l'endroit où elle recherche une mémoire vidéo (nous allons passer un peu de temps à l'étudier).
  • Trouvez le schéma de circuit de la machine d'arcade et suivez les signaux des contacts d'adresse de la CPU. Voyez où ils vont. Par exemple, A15 (adresse la plus ancienne) ne peut accéder qu'à la ROM. De cela, nous pouvons conclure que les adresses de la ROM commencent à 8000 $.

Il peut être très intéressant et instructif de le découvrir vous-même en observant l'exécution du code. Quelqu'un a dû faire face à tout cela pour la première fois.

Développement en ligne de commande


Le but de ce didacticiel n'est pas de vous apprendre à écrire du code pour une plate-forme spécifique, bien que nous ne puissions pas éviter le code spécifique à la plate-forme. J'espère qu'avant le début du projet, vous saviez déjà comment compiler pour votre plateforme cible.

Lorsque vous travaillez avec du code autonome, qui lit simplement des fichiers et affiche du texte dans la console, il n'est pas nécessaire d'utiliser un système de développement trop compliqué. En fait, cela ne fait que compliquer les choses. Tout ce dont vous avez besoin est un éditeur de texte et un terminal.

Je pense que quiconque veut programmer à un niveau bas devrait savoir comment créer des programmes simples à partir de la ligne de commande. Vous pouvez considérer que je vous taquine, mais vos compétences de pirate d'élite ne valent pas grand-chose si vous ne pouvez pas fonctionner en dehors de Visual Studio.

Sur Mac, vous pouvez utiliser TextEdit et Terminal pour compiler. Sous Linux, vous pouvez utiliser gedit et Konsole. Sous Windows, vous pouvez installer cygwin et les outils, puis utiliser N ++ ou un autre éditeur de texte. Si vous voulez être vraiment cool, alors toutes ces plates-formes prennent en charge vi et emacs pour l'édition de texte.

La compilation de programmes à partir d'un seul fichier à l'aide de la ligne de commande est une tâche triviale. Supposons que vous ayez enregistré votre programme dans un fichier appelé 8080dis.c . Accédez au dossier contenant ce fichier texte et compilez-le comme cc 8080dis.c : cc 8080dis.c . Si vous ne spécifiez pas le nom du fichier de sortie, il sera appelé a.out et vous pouvez l'exécuter en tapant ./a.out .

En fait, c'est tout.

Utiliser un débogueur


Si vous travaillez sur l'un des systèmes basés sur Unix, voici une brève introduction au débogage de programmes en ligne de commande à l'aide de GDB. Vous devez compiler le programme comme ceci: cc -g -O0 8080dis.c . Le paramètre -g génère des informations de débogage (c'est-à-dire que vous pouvez effectuer le débogage en fonction du texte source) et le paramètre -O0 désactive les optimisations de sorte que lorsque vous parcourez le programme, le débogueur puisse suivre avec précision le code en pleine conformité avec le texte source.

Voici le journal annoté du début d'une session de débogage. Mes commentaires sont sur des lignes marquées d'un signe dièse (#).

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

Démonteur, partie 2


Exécutez le désassembleur du fichier ROM invaders.h et examinez les informations affichées.

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

Les premières instructions correspondent à celles que nous avons notées manuellement précédemment. Après eux, il y a plusieurs nouvelles instructions. Ci-dessous, j'ai inséré des données hexadécimales pour référence. Notez que si vous comparez la mémoire avec les commandes, les adresses sont comme si elles étaient stockées en mémoire dans l'ordre inverse. Il en est ainsi. C'est ce qu'on appelle le petit endian - les machines avec peu d'endian, comme le 8080, stockent d'abord les octets de nombres les moins significatifs. (Plus sur l'endian est décrit ci-dessous.)

J'ai mentionné ci-dessus que ce code est le code ISR du jeu Space Invaders. Le code des interruptions 0, 1, 2, ... 7 commence par l'adresse $ 0, $ 8, $ 20, ... $ 38. Il semble que le 8080 ne donne que 8 octets pour chaque ISR. Parfois, le programme Space Invaders contourne ce système en se déplaçant simplement vers une autre adresse avec plus d'espace. (Cela se produit à 000c $).

De plus, ISR 2 semble être plus long que la mémoire qui lui est allouée. Son code passe à 0018 $ (c'est l'endroit pour ISR 3). Je pense que Space Invaders ne s'attend pas à voir quoi que ce soit qui utilise l'interruption 3.

Le fichier ROM Space Invaders sur Internet se compose de quatre parties. Je vais l'expliquer ci-dessous, mais pour l'instant, pour passer à la section suivante, nous devons fusionner ces quatre fichiers en un seul. Sous Unix:

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

Exécutez maintenant le désassembleur avec le fichier «envahisseurs» résultant. Lorsqu'un programme démarre à $ 0000, la première chose qu'il fait est de passer à $ 18d4. Je considérerai cela comme le début du programme. Jetons un coup d'œil à ce code.

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

Ainsi, il effectue deux opérations et appelle $ 01e6. Je vais insérer une partie du code avec des transitions dans ce code:

  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 

Comme nous l'avons vu à partir de l'allocation de mémoire Space Invaders, certaines de ces adresses sont intéressantes. 2000 $ est le début d'un programme de «RAM de travail». 2400 $ est le début de la mémoire vidéo.

Ajoutons des commentaires au code pour expliquer ce qu'il fait directement au démarrage:

  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 

Il semble que ce code copiera 256 octets de 1b00 $ à 2000 $. Pourquoi? Je ne sais pas. Vous pouvez étudier le programme plus en détail et réfléchir à ce qu'il fait.

Il y a un problème ici. Si nous avons un morceau de mémoire arbitraire contenant du code, alors les données alterneront probablement avec lui.

Par exemple, les sprites pour les personnages du jeu peuvent être mélangés avec du code. Lorsqu'un désassembleur tombe dans un tel fragment de mémoire, il pense que c'est du code et continue de le «mâcher». Si vous n'avez pas de chance, tout code désassemblé après cette donnée peut être incorrect.

Bien que nous ne puissions presque rien y faire. Gardez juste à l'esprit qu'un tel problème existe. Si vous voyez quelque chose comme ça:

  • transition d'un code exactement bon à une équipe qui ne figure pas dans la liste des désassembleurs
  • flux de code sans signification (par exemple POP B POP B POP B POP C XTHL XTHL XTHL)

ici, probablement, il y a des données qui ont ruiné une partie du code démonté. Si cela se produit, vous devez recommencer à partir du décalage.

Il s'avère que les Space Invaders rencontrent périodiquement des zéros. Si notre démontage s'arrête un jour, les zéros le forceront à effectuer une réinitialisation.

Une analyse détaillée du code Space Invaders peut être trouvée ici .

Endian


Les octets sont stockés différemment dans différents modèles de processeur, et le stockage dépend de la taille des données. Les machines big-endian stockent les données des plus anciennes aux plus jeunes. Le petit-endian les garde du plus jeune au plus vieux. Si un entier 32 bits 0xAABBCCDD est écrit dans la mémoire de chaque machine, il ressemblera à ceci:

En petit-boutien: $ DD $ CC $ BB $ AA

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

J'ai commencé à programmer sur des processeurs Motorola qui utilisaient le big-endian, donc cela me semblait plus «naturel», mais ensuite je me suis habitué au little-endian.

Mon désassembleur et mon émulateur évitent complètement le problème endian car ils ne lisent qu'un octet à la fois. Si vous souhaitez, par exemple, utiliser un lecteur 16 bits pour lire l'adresse de la ROM, notez que ce code n'est pas portable entre les architectures CPU.

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


All Articles