Le but de ce TP est de proposer un désassembleur chip8, c’est-à-dire un programme capable de lire des programmes (ROM) pour plateforme à base de chip8 et de proposer le code assembleur représenté sous forme textuelle plutôt que sous forme numérique. Le chip8 n’est pas un processeur, mais plus l’équivalent d’une machine virtuelle. Néanmoins, son architecture et son mode de fonctionnement sont assez proche de ce que l’on peut trouver dans un processeurs.

Représentation d’une machine

Une machine chip8 est constituée (entre autre) :

  • d’une mémoire,

  • d’un ensemble de 16 registres (notés V1, …​, VF ) de 8bits,

  • d’un pseudo-registre d’accés à la mémoire I de 12bits,

  • d’un “programme counter” (PC) de 16bits.

Note : le chip8 n’est capable de “comprendre” que des entiers positifs, on ne travaillera donc que sur des valeur unsigned systématiquement (dans les structures, variables locales et paramètres). Par exemple : unsigned short…​

Le chip8 est capable d’accéder à 4096 octets de RAM, depuis l’adresse 0x000 (0) à 0xFFF (4095). Le pseudo-registre PC sert à stocker l’adresse de l’instruction en mémoire qui va être exécutée.

  1. Pour proposer une structure capable de représenter une machine Chip8, il faut pouvoir être capable de représenter toutes les composantes de la machine dans une structure. De combien de champs va-t-on avoir besoin dans cette structure et quels vont être leurs types ?

  2. Écrivez une structure capable de représenter une machine Chip8.

Comme la plupart des APIs qui manipulent des données en C et en procédural, il est nécessaire de donner 2 fonctions principales :

  • une fonction pour la création des donnée,

  • une fonction pour l’initialisation de la donnée créée.

    1. Écrivez une fonction qui permet de créer une donnée de la structure que vous avez définit précédemment.

    2. Écrivez une fonction qui va initialiser la donnée créée, c’est-à-dire, qui va initialiser tout les champs de la structure aux bonnes valeurs de départ (en général, tout à 0).

Chargement d’une ROM

Vous pourrez trouver des ROM pour chip8 un peu partout sur le net (ex: https://www.zophar.net/pdroms/chip8.html). Vérifier juste qu’il sagit bien de ROM pour le chip8 et pas le super chip8. Une ROM est l’équivalent d’un "morceau de mémoire" mis dans un fichier. Cela signifie qu’elle contient l’état de la mémoire à un instant précis. En conséquence, lorsque l’on veut charger une ROM, il convient donc de lire le fichier contenant la ROM et de le stocker dans la mémoire de notre Chip8 (donc dans le champs mémoire de notre donnée représentant le Chip8). Le chargement d’une ROM se fait par lecture du fichier en mode binaire, puis en lisant octet par octet. Chaque octet est stocké dans la mémoire de la machine à partir de l’adresse 0x200 (contrainte du chip8).

  1. Écrivez une fonction qui lit un fichier contenant une ROM et la stocke dans la mémoire d’une machine (rappel : vous utiliserez les fonctions fopen, fread et fclose).

Décodage d’une instruction

Une instruction chip8 est codée sur 16bits (2 octets). Par exemple : 0x00E0 représente l’instruction qui demande l’effacement de l’écran. Lorsque l’on veut décoder une instruction, il est donc nécessaire de lire 2 octets (2 unsigned chars) en mémoire à l’adresse pointé par PC, et de placer les 2 octets dans une seule variable de plus grande capacité (unsigned short).

  1. Proposez une fonction qui prend deux unsigned char en paramètre et qui retourne le résultat "concaténé" des 2 unsigned chars. Par exemple, avec a = 0xAB et b = 0xCD, le résultat sera une variable de type unsigned short avec 0xABCD. Pour pouvoir faire ceci, il est nécessaire d’effectuer des opérations de décalage binaire et de jouer avec l’opération ||.

  2. Écrivez une fonction qui retourne l’instruction (le unsigned short) présente en mémoire d’une machine à l’adresse indiquée par PC.

Anatomie d’une instruction

Une instruction peut être constituée d’un certains nombre de morceaux qui donne des informations sur l’action qu’elle représente. Par exemple 0x6633 représente le chargement de la valeur 0x33 dans le registre V6. De manière un peu plus générale, les instructions qui ont la forme 0x6RJJ représente le chargement d’une valeur 0xJJ dans le registre VR. Nous n’allons pas décoder toutes les instructions chip8 (il en existe 35), mais juste quelques unes (vous pouvez toutes les faire si vous le souhaitez). Le but des fonctions suivante vont être de produire un affichage plus intelligible que 0x6633 en affichant mov V6, 0x33 (si l’on reprend l’exemple précédent).

Les instructions que nous allons décoder sont les suivantes :

0x00E0  → CLS
0xBjjj  → JMP jjj
0x6rjj  → MOV Vr, jj
0x7rjj  → ADD Vr, jj
0x8ry4  → ADD Vr, Vy
0xAjjj  → MOV I, jjj
UNKNOWN → fonction de fallback si inst pas encore codé

Pour simplifier l’identification de chaque informations de chaque instructions, vous allez écrire 4 fonctions : part1, part2, part3 et part4 qui vont isoler une partie des bits du unsigned short passé en paramètre de la fonction. Par exemple : part1(0x6234) va retourner 0x6000, part3(0x6234) va retourner 0x0200…​etc

  1. Écrivez les fonction part1, part2, part3 et part4 (vous utiliserez des masques pour faire ceci).

  2. Proposez une fonction par instruction qui prend l’entier représentant l’instruction en paramètre et propose un affichage de en mode texte (on sait que l’instruction sera la bonne passée en paramètre, pas besoin de tester ici).

  3. Proposez une fonction qui exécute la bonne fonction d’affichage en fonction de l’instruction passée en paramètre. si l’instruction n’est pas encore connue, alors la fonction de fallback est appelée.

Dump de la mémoire

Il faut maintenant mettre tout en commun pour proposer un dump de la mémoire. La boucle de décodage suit la logique suivante :

pour i = 0..fin de la mémoire:
    inst = fetch_inst(machine)
    decode_affiche(inst)
    machine.PC = machine.PC + 1
  1. Écrivez une fonction qui code ce comportement

  2. Écrivez le main terminal qui prend en paramètre de la ligne de commande une ROM à dump

S’il vous reste du temps

  1. Vous pouvez implémenter l’affichage des autres instructions.

  2. Pour l’instant, l’algo de dump balaye toute la mémoire même si la ROM chargée est petite. Trouvez un moyen de ne parcourir que l’équivalent du programme chargé en mémoire plutôt que tout.

  3. Vous pouvez aussi code le comportement des instructions et leur impact sur la machine faisant tourner le code (assignation de registres, affichage en mode texte…​etc). De cette façon, vous pouvez coder un émulateur Chip8 quasi complet.

Associate professor in Computer Sciences

My research interests include language engineering, virtual machine and software engineering in general.