Projet de Machines Virtuelles Gabriel Kerneis Vendredi 11 mars 2011 1 Introduction Le but de ce sujet est d implémenter la MARS (Memory Array Redcode Simulator), une machine virtuelle utilisée dans le jeu Core War. Le Core War est un jeu de programmation inventé en 1984 dans lequel deux programmes s affrontent pour contrôler la machine. Ces programmes sont écrits dans un langage assembleur, le Redcode. Le vainqueur est celui qui parvient à arrêter tous les processus du programme adversaire, restant seul en mémoire. Historiquement, le Core War est dérivé d un autre jeu, Darwin, inventé en 1961. Darwin se jouait directement dans la mémoire physique des serveurs IBM 7090, sans aucune protection (les participants s engageaient à ne pas tricher et écrire dans des zones mémoires interdites). Vous imaginez facilement l intérêt d une machine virtuelle dans ce contexte. Il existe de très nombreuses variantes du Core War, plus ou moins bien documentées ; ce sujet s inspire de la définition ICWS 88. Si vous voulez en savoir plus, il existe plusieurs sites consacrés à Core War, parmi lesquels le plus actif est sans doute http://corewar.co.uk/. Une bonne introduction à l assembleur Redcode est The beginners guide to Redcode 1. Attention, à partir de la section The instruction modifiers, l auteur aborde des spécificités de la définition ICWS 94 qui ne figurent pas dans le Redcode de ce sujet. 2 Architecture de la MARS Fonctionnement général Les programmes Core War résident dans une mémoire commune. Ils disposent chacun d un ensemble de processus, ordonnancés en file circulaire (round-robin). À chaque cycle, chaque programme exécute une instruction du premier processus de sa file. Il existe quatre types d instructions : Données : l instruction DAT n est pas exécutable : un processus qui tente d exécuter une instruction DAT meurt instantanément. Un programme a perdu lorsque tous ses processus sont morts. Le but d un programme est d amener tous les processus de son adversaire à exécuter des instructions DAT tout en gardant lui-même au moins un processus vivant. Création de processus : l instruction SPL ajoute un nouveau processus à la file de processus du programme. Écriture en mémoire : les instructions MOV, ADD et SUB permettent de copier, ajouter ou retrancher des valeurs en mémoire. Saut conditionnel : les instructions JMP, JMZ, JMZ, CMP et SLT effectuent des sauts conditionnels variés. L instruction DJN est un saut spécial qui décrémente au passage une valeur en mémoire. 1. http://vyznev.net/corewar/guide.html 1
Mémoire (core) La mémoire de la MARS a une structure très particulière : elle est cyclique et à adressage relatif. On la représente par une liste d instructions de taille M (M 8192). Comme la mémoire est cyclique, toutes les opérations sont effectuées sur des entiers positifs modulo M ; un processus qui tenterait d écrire à l adresse M + 42, par exemple, écrirait en fait à l adresse 42. L adressage relatif, à l aide d offsets, résulte du fait qu il n y a aucune instruction qui permette à un processus de connaître son adresse courante (notée PC), ou d accéder à une adresse absolue arbitraire. Par exemple, un processus pourra désigner l adresse «3 instructions plus loin» (offset +3, correspondant à l adresse absolue PC + 3) ou «2 instructions avant» (offset M 2 2, correspondant à l adresse absolue PC 2). Files de processus (task queues) Chaque processus est représenté par son program counter (PC), c est-à-dire par l adresse mémoire absolue de son instruction courante. L ensemble des processus d un programme est stocké dans une file de taille bornée. C est toujours le premier processus qui est retiré de la file puis exécuté. En fin de cycle, l adresse mise à jour est ajoutée en queue de file, sauf pour les instructions DAT (qui tue le processus) et SPL (qui rajoute non pas une mais deux adresses en queue de file). 3 Cycle d exécution 3.1 Démarrage et arrêt Au démarrage, la MARS reçoit en paramètre le code de deux programmes et l adresse à laquelle chacun doit être chargé. Elle initialise sa mémoire avec des instructions DAT # 0, # 0, puis elle copie chaque programme en mémoire à l adresse indiquée. Elle ajoute enfin l adresse de chargement de chaque programme dans sa file de processus et commence le premier cycle d exécution. Chaque cycle d exécution de la MARS comprend un cycle fetch-decode-execute par programme. Les programmes sont exécutés suivant l ordre dans lequel ils ont été chargés en mémoire. La MARS exécute toujours un cycle complet, même dans le cas où le premier des deux programmes meurt à cause d une instruction DAT. À la fin de chaque cycle, la MARS vérifie s il y a un vainqueur : si les deux files de processus sont vides, il y a égalité, sinon, si l une des files est vide, c est l autre programme qui est déclaré vainqueur, sinon, si la MARS a exécuté 100 000 cycles, il y a égalité, sinon, la MARS exécute le cycle suivant. 3.2 Fetch Le premier processus, désigné par son adresse PC, est retiré de la file de processus du programme courant. L instruction à l adresse absolue PC est ensuite récupérée en mémoire pour être décodée et exécutée. 3.3 Decode Une instruction Redcode est constituée de cinq champs : l opcode, le A-mode et le A-nombre, le B-mode et le B-nombre. L ensemble d un mode et d un nombre est appelé opérande (on parle 2. Rappelez-vous qu on travaille avec des entiers positifs modulo M. 2
du A-opérande et du B-opérande). À partir de ces opérandes, la phase de décodage consiste à calculer deux adresses mémoire absolues, le A-pointeur et le B-pointeur. Modes Les modes sont au nombre de quatre : immédiat (noté #), direct (mode par défaut), indirect (noté @) et indirect pré-décrémenté (noté <). Ils modifient la manière dont le A-nombre et le B-nombre sont interprétés dans le calcul du A-pointeur et du B-pointeur. En mode immédiat, l instruction utilise le nombre tel quel (par exemple en tant que valeur à ajouter pour l instruction ADD) ; le pointeur associé est simplement PC. En mode direct, le nombre représente un offset : le pointeur est la somme de PC et du nombre. En mode indirect, le nombre est également interprété comme un offset vers une instruction ; mais c est ensuite le B-nombre 3 de cette instruction intermédiaire qui, interprété à nouveau comme un offset, donne la valeur du pointeur. Enfin, en mode indirect pré-décrémenté, le B-nombre de l instruction intermédiaire est décrémenté avant de calculer le second offset. Algorithme de décodage Les étapes de décodage précises sont données Figure 1. Il est important de respecter scrupuleusement l ordre indiqué, car le mode indirect pré-décrémenté modifie certaines valeurs en mémoire qui peuvent être réutilisées dans la suite du décodage. Notez que les deux opérandes seront toujours décodés, même si l instruction (par exemple, DAT) ne les utilise pas. On rappelle que tous les calculs se font sur des entiers positifs modulo M. A-pointeur B-pointeur 1. Si le A-mode est «immédiat», le A-pointeur est égal à PC. 2. Sinon, on ajoute le A-nombre et PC pour obtenir une adresse L. 3. Si le A-mode est «direct», le A-pointeur vaut L. 4. Sinon, on lit le B-nombre n de l instruction à l adresse L. 5. Si le A-mode est «indirect», le A-pointeur vaut L + n. 6. Si le A-mode est «indirect pré-décrémenté», on écrit n 1 dans le B-nombre de l instruction à l adresse L et le A-pointeur vaut L + n 1. 1. Si le B-mode est «immédiat», le B-pointeur est égal à PC. 2. Sinon, on ajoute le B-nombre et PC pour obtenir une adresse L. 3. Si le B-mode est «direct», le B-pointeur vaut L. 4. Sinon, on lit le B-nombre n de l instruction à l adresse L. 5. Si le B-mode est «indirect», le B-pointeur vaut L + n. 6. Si le B-mode est «indirect pré-décrémenté», on écrit n 1 dans le B-nombre de l instruction à l adresse L et le B-pointeur vaut L + n 1. Figure 1 Décodage d une instruction 3.4 Execute Une fois le décodage effectué, la phase d exécution modifie certaines valeurs en mémoire, calcule la nouvelle valeur du program counter et l ajoute à la file des processus du programme courant (sauf pour les instructions DAT et SPL). 3. Il s agit bien toujours du B-nombre, même pour le calcul du A-pointeur. 3
Par souci de concision, on utilisera dans la suite les termes A-instruction pour désigner l instruction située à l adresse du A-pointeur et B-instruction celle située à l adresse du B- pointeur. De plus, lorsque les termes A-mode, A-nombre, B-mode et B-nombre sont utilisés sans plus de précision, il s agit des champs de l instruction courante. DAT A B L instruction DAT ne fait rien (et cause donc la mort du processus courant). MOV A B Si l A-mode est immédiat, le A-nombre est copié dans le B-nombre de la B-instruction. Sinon, l A-instruction est copiée intégralement à l adresse du B-pointeur. On ajoute ensuite PC + 1 à la file des processus du programme courant. ADD A B Si l A-mode est immédiat, le A-nombre est ajouté au B-nombre de la B-instruction. Sinon, les A et B-nombres de l A-instruction sont ajoutés, respectivement, aux A et B-nombres de la B-instruction. On ajoute ensuite PC + 1 à la file des processus du programme courant. SUB A B Si l A-mode est immédiat, le A-nombre est retranché du B-nombre de la B-instruction. Sinon, les A et B-nombres de l A-instruction sont retranchés, respectivement, des A et B-nombres de la B-instruction. On ajoute ensuite PC + 1 à la file des processus du programme courant. JMP A B Le A-pointeur est ajouté à la file des processus du programme courant. JMZ A B Si le B-nombre de la B-instruction est égal à 0, le A-pointeur est ajouté à la file des processus du programme courant. Sinon, on ajoute PC + 1 à la file des processus du programme courant. JMN A B Si le B-nombre de la B-instruction est différent de 0, le A-pointeur est ajouté à la file des processus du programme courant. Sinon, on ajoute PC+1 à la file des processus du programme courant. CMP A B Si le A-mode est immédiat, on compare le A-nombre au B-nombre de la B-instruction. Sinon, on compare l A-instruction et la B-instruction (champ par champ). Si les valeurs comparées sont égales, l instruction suivante est sautée, c est-à-dire qu on ajoute PC + 2 à la file des processus du programme courant. Sinon, on ajoute PC + 1 à la file des processus du programme courant. SLT A B Si le A-mode est immédiat, on note X le A-nombre, et Y le B-nombre de la B-instruction. Sinon, on note X le B-nombre de l A-instruction, et Y le B-nombre de la B-instruction. Si X < Y, l instruction suivante est sautée, c est-à-dire qu on ajoute PC + 2 à la file des processus du programme courant. Sinon, on ajoute PC + 1 à la file des processus du programme courant. DJN A B On décrémente le B-nombre de la B-instruction, puis on agit exactement comme pour l instruction JMN A B. SPL A B On ajoute PC + 1 à la file des processus du programme courant. Le A-pointeur est ensuite ajouté à la file des processus du programme courant, à condition que celle-ci n ait pas atteint sa taille maximale. 4
4 Format binaire La MARS utilise un format binaire pour représenter les instructions. Un programme, ou une copie de la mémoire (core dump), sont simplement représentés comme la concaténation des instructions qui les composent. Un assembleur-désassembleur est fourni avec le sujet pour vous faciliter l écriture de programmes et l analyse du format binaire. Pour plus de détails sur le langage assembleur, vous pouvez vous référer au Beginners Guide indiqué en introduction. Instruction Une instruction est codée sur cinq octets : un pour l opcode et deux pour chaque opérande (Figure 2). b 7... b 0 b 15... b 0 b 15... b 0 opcode A-opérande B-opérande Figure 2 Instruction Opcode L opcode est codé sur un octet. Les trois bits de poids faible sont réservés et doivent être égaux à zéro. Les cinq bits de poids fort représentent un entier non-signé compris entre 0 et 10 (Figure 3). On associe à chaque entier l un des onze opcodes suivants : DAT (0), MOV (1), ADD (2), SUB (3), JMP (4), JMZ (5), JMN (6), CMP (7), SLT (8), DJN (9) et SPL (10). b 7 b 6 b 5 b 4 b 3 b 2 b 1 b 0 opcode réservé Figure 3 Opcode Opérandes Les opérandes sont des entiers non-signés de 16 bits en codage gros-boutiste (bigendian). Les 3 bits de poids fort représentent le mode de l opérande, et les 13 bits de poids faible son nombre (Figure 4). Le nombre est un entier non-signé compris entre 0 et M 1. Le mode est un entier non-signé compris entre 0 et 3. On associe à chaque entier l un des quatre modes suivants : immédiat (0), direct (1), indirect (2) et indirect pré-décrémenté (3). b 15 b 14 b 13 b 12 b 11 b 10 b 9 b 8 b 7 b 6 b 5 b 4 b 3 b 2 b 1 b 0 mode nombre Figure 4 Opérande 5
5 Implémentation demandée Pour avoir la moyenne, un projet doit implémenter l interface minimale décrite ci-dessous et passer tous les tests fournis. Pour ceux qui désirent aller au-delà, des idées d extensions sont proposées. Un programme qui ne passe pas les jeux de test fournis ne sera pas accepté comme solution à ce projet. Tout projet doit être rendu accompagné d un rapport au format pdf décrivant la démarche, les choix d implémentations et documentant les extensions éventuelles. Vous pouvez coder, au choix, en C, Java ou OCaml. Les modalités de rendu seront précisées sur la liste de discussion. 5.1 Interface minimale L interaction avec la MARS se fait par l intermédiaire de la ligne de commande. La MARS attend quatre paramètres : le nom de fichier du premier programme, l adresse à laquelle le premier programme doit être chargé en mémoire, le nom de fichier du second programme et l adresse à laquelle le second programme doit être chargé en mémoire. Par défaut, la MARS utilise une mémoire de 8192 instructions et limite le nombre de processus par programme à 64. La MARS affiche sur sa sortie standard une unique ligne, sur le modèle suivant : winner=1,q1=5,q2=0,cycles=2546 Le champ winner vaut 0 en cas d égalité (que les deux programmes soient morts lors du même cycle, ou que le nombre maximum de cycles ait été atteint) ; sinon, il est égal au numéro du programme vainqueur. Les champs q1 et q2 indiquent la longeur de chaque file à la fin du dernier cycle, et cycles le nombre de cycles exécutés. Tout autre texte doit être dirigé vers la sortie d erreur. Au moment de quitter, la MARS écrit une copie de la mémoire telle qu elle était à la fin du dernier cycle dans un fichier core.dump (dans le répertoire courant). Le code de retour de la MARS est 0 en cas d exécution réussie, et une autre valeur à votre choix en cas d erreur (programme invalide, par exemple). 5.2 Idées d extension Ajouter des options pour modifier la taille de la mémoire, le nombre processus par programme, ou encore charger les programmes à des adresses aléatoires. Supporter l exécution de plus de deux programmes simultanément. Ajouter une interface graphique pour visualiser les combats. Définir de nouveaux opcodes, de nouveaux modes (vous pouvez implémenter la définition ICWS 94 si vous êtes courageux). Pensez à proposer et documenter vos extensions au format binaire sur la liste de discussion pour que d autres puissent aussi les implémenter. Écrire des programmes Redcode, mesurer les performances de programmes existants. Implémenter un mode «tournoi» où la MARS confronte les programmes deux à deux et affiche un classement. 6