II. Rappels sur le microprocesseur Intel 8086
Objectif : Au cours de ce TD, nous allons voir les notions fondamentales ...
Comment établir un plan mémoire d'un système à microprocesseur existant.
part of the document
ce projet est de créer une application permettant de simuler lexécution dun programme écrit dans un langage dassemblage ressemblant à celui du microprocesseur 8086. Cette simulation ayant pour but de faciliter la compréhension dun langage dassemblage, elle se doit de présenter clairement lordre dexécution des instructions, leur impact sur les données, ainsi que sur les différents éléments manipulés par ce microprocesseur, que sont la pile, les registres et les flags.
Il existe déjà de tels débogueurs dans le commerce, mais lapprentissage de leur syntaxe et de leur mode de fonctionnement nécessite de passer de nombreuses heures à compulser la documentation. Notre but est de créer un interpréteur minimaliste, dun sous-ensemble du langage dassemblage du 8086, très rapidement compréhensible, même pour un utilisateur nayant que peu de connaissances sur ce microprocesseur.
Le langage retenu pour effectuer le développement de cette application est Java. Lintérêt dutiliser ce langage est que Java est un langage objets, ce qui a permis deffectuer une modélisation du problème sous forme de classes. A cela sajoutent les fonctionnalités offertes par ce langage en terme dInterfaces Homme-Machine.
Dans un premier temps, nous allons présenter succinctement les principales caractéristiques du microprocesseur Intel 8086. Ensuite nous aborderons nos choix dimplémentation, la modélisation effectuée et le fonctionnement de la partie émulant le 8086. Enfin, nous nous intéresserons plus particulièrement à lutilisation de linterface graphique de lapplication.
II. Rappels sur le microprocesseur Intel 8086
Développé en 1978 par Intel, le microprocesseur 8086 est un processeur CISC 16 bits. Il sagit du premier microprocesseur de la famille des « x86 » comprenant les célèbres 80386 et 80486.
Ses caractéristiques principales sont les suivantes :
Bus de données dune largeur de 16 bits.
Bus dadresses de 20 bits, ce qui permet dadresser un total de 1 mégaoctet de mémoire.
14 registres de 16 bits dont un registre détat contenant des indicateurs binaires.
La mémoire est segmentée en 16 blocs de 64 Ko et une adresse sur 20 bits est obtenue en combinant deux parties :
Le registre CS permet de stocker les 4 bits de poids fort donnant le numéro de segment de mémoire ;
Le registre IP fournit les 16 bits de poids faible donnant ladresse à lintérieur du segment de mémoire spécifié par CS.
Ladresse mémoire est retrouvée selon la formule :
adresse = (16 x CS) + IP.
Le 8086 autorise un mode de fonctionnement en pas à pas, ainsi que lutilisation dopérations spécifiques appelées interruptions permettant au 8086 de « dialoguer » avec les autres périphériques de lordinateur.
Les registres du 8086 se décomposent en 4 grandes familles :
4 registres de données, se décomposant chacun en deux parties : une partie « haute » et une partie « basse » de 8 bits chacune, ce qui permet au microprocesseur de manipuler des données sur 8 ou 16 bits :
AX (décomposable en AH et AL) sert daccumulateur et est principalement utilisé lors dopérations arithmétiques et logiques ;
BX est la plupart du temps utilisé comme opérande dans les calculs ;
CX est utilisé comme compteur dans les structures itératives ;
DX, tout comme AX, est utilisé pour les calculs arithmétiques et notamment dans la division et la multiplication. Il intervient également dans les opérations dentrées/sorties.
registres de segmentation :
CS (segment de code) permet de déterminer les adresses sur 20 bits ;
DS (segment de données) ;
SS (segment de pile) ;
ES (segment supplémentaire).
registres pointeurs ou dindex :
SP (pointeur de pile) pointe sur le sommet de la pile de données ;
BP (pointeur de base) pointe sur la base de la pile de données ;
SI (index de source) ;
DI (index de destination).
pointeur dinstruction : IP stocke ladresse de la prochaine instruction à exécuter par le microprocesseur.
registre spécial contenant 9 indicateurs binaires nommés « flags » :
AF (indicateur de retenue auxiliaire) ;
CF (indicateur de retenue) est mis à 1 lorsquil y a eu une retenue lors dun calcul ;
OF (indicateur de débordement) est mis à 1 lorsquun débordement arithmétique a eu lieu (lorsque le résultat dune opération ne peut tenir sur 16 bits) ;
SF (indicateur de signe) représente le signe du résultat dune opération (0 = positif, 1 = négatif) ;
PF (indicateur de parité) est mis à 1 lorsque le résultat dune opération contient un nombre pair de 1 ;
ZF (indicateur de zéro) est mis à 1 lorsque le résultat dune opération vaut 0 ;
DF (indicateur de direction) ;
IF (indicateur dautorisation dinterruption) ;
TF (indicateur dinterruption pas à pas).
Le 8086 est programmable dans un langage dassemblage comportant des instructions utilisant les registres, les flags, la mémoire et, en ce qui concerne les interruptions, dautres éléments de lordinateur. Voici un aperçu des instructions les plus couramment utilisées (dans la liste qui suit, les mots entre parenthèses indiquent le nom de linstruction). Ces instructions seront étudiées plus en détail dans la section consacrée à la partie « CPU » de notre émulateur.
Instructions arithmétiques : addition (ADD), soustraction (SUB), multiplication (MUL), division (DIV), incrémentation (INC), décrémentation (DEC) et échange (XCHG) ;
Instructions logiques : et (AND), ou (OR) et non (NOT) ;
Instruction de comparaison (CMP) : met à jour les flags pour permettre lutilisation des instructions de saut ;
Instructions de saut : saut si égal (JE), saut si différent (JNE), saut si inférieur (JL),
Instructions de gestion de la pile : empilement (PUSH) et dépilement (POP)
Instruction dappel (CALL) et de retour (RET) ;
De nombreuses autres instructions que nous ne détaillerons pas ici.
III. Modélisation de lémulateur
1. Caractéristiques générales
Simuler complètement le fonctionnement dun 8086 aurait demandé un travail considérable car ses propriétés sont nombreuses. La durée imposée pour le stage nous a conduit à sélectionner celles qui nous ont paru essentielles.
La gestion de la mémoire à la manière du 8086 nétant pas non plus lobjectif principal de lémulateur, nous avons opté pour un système à mi-chemin entre celle-ci et celle proposée dans un langage impératif comme le Pascal. Cette gestion passe donc par lutilisation de variables et de tableaux, au lieu de segments. Le type dune variable ou dun élément dun tableau est systématiquement un entier non signé sur 16 bits, ce qui autorise les valeurs de 0 à 65535.
Au niveau du processeur, les registres supportés sont au nombre de 7 : les registres de données (AX, BX, CX, DX), les deux registres pointeurs de pile (BP et SP) et le pointeur dinstruction IP. Les autres registres ont été laissés de coté en raison de leur utilisation dans les interruptions (abandonnées du fait dune trop grande complexité) et la gestion de la mémoire par segments. Les flags conservés sont ZF, CF, OF et SF, les autres ayant été rejetés car nintervenant pas dans les instructions que nous avons choisi de supporter.
La pile, quand à elle, est gérée non pas comme une partie de la mémoire centrale mais comme une pile de données pouvant contenir des adresses et des valeurs.
Le langage dassemblage supporté par notre émulateur a été déterminé en réalisant un compromis entre le langage dassemblage réel du 8086 et notre besoin de supporter les variables et les tableaux. Le besoin de réaliser des sauts dune instruction à une autre sans passer par la mémoire ont été rendus possibles par lutilisation détiquettes placées dans le texte du programme écrit dans notre langage.
Un programme est écrit sous forme de texte et doit comporter deux sections :
la première, introduite par le mot-clé DATA seul sur une ligne, est la partie de déclaration des variables et des tableaux. Dans cette partie, on peut déclarer une variable ou un tableau par ligne, avec la syntaxe suivante pour une variable :nom_de_variable DW valeur_initialeet celle-ci pour un tableau :nom_de_tableau DW[taille]A titre de rappel, notre émulateur ne permet que de supporter les données représentées par des mots non signées sur 16 bits, ce qui nautorise que les valeurs entières comprises entre 0 et 65535.
la seconde, introduite par le mot-clé CODE seul sur une ligne, qui se termine à la fin du fichier, consiste en une liste dinstructions à exécuter. Ces instructions peuvent comporter des étiquettes, et faire intervenir les variables et les tableaux déclarés dans la section DATA, ainsi que les registres et des constantes numériques. Chaque ligne ne peut contenir quune instruction ou une étiquette.Un programme peut en outre comporter des lignes vides ou des lignes de commentaires, lesquelles doivent chacune débuter par un point-virgule.
Les instructions supportées sont les suivantes :
NomFormes acceptéesDescriptionPUSHPUSH regEmpile le contenu du registrePOPPOP regDépile dans le registre spécifiéMOVMOV reg1, reg2MOV reg , varMOV var , regMOV reg , csteMOV reg1 , tableau[reg2]MOV tableau[reg1], reg2Affecte le contenu du second paramètre au premier paramètre. Le second paramètre reste inchangé.CALLCALL etiquetteBranchement sur une étiquette. La ligne depuis lequel sest fait le branchement est placée dans la pile.RETRETRetour à une ligne dont le numéro a été placé en sommet de pile.ADDADD reg , csteADD reg1, reg2ADD reg , varADD reg1, tableau[reg2]Addition des valeurs des deux paramètres. Le résultat est stocké dans le premier paramètre, tandis que le second reste inchangé. Les flags sont mis à jour.SUBSUB reg , csteSUB reg1, reg2SUB reg , varSUB reg1, tableau[reg2]Soustraction des valeurs des deux paramètres. Le résultat est stocké dans le premier paramètre, tandis que le second reste inchangé. Les flags sont mis à jour.MULMUL csteMUL regMUL varMUL tableau[reg]Multiplication de la valeur du paramètre par le contenu du registre AX. Le résultat est stocké dans AX. Les flags sont mis à jour.DIVDIV csteDIV regDIV varDIV tableau[reg]Division entière du contenu du registre AX par la valeur du paramètre. Le quotient est stocké dans AX, et le reste est placé dans DX. Les flags sont mis à jour. En cas de division par zéro, lexécution est interrompue.CMPCMP reg , csteCMP reg1, reg2CMP reg , varCMP reg1, tableau[reg2]Soustraction de la valeur du second paramètre à la valeur du premier paramètre. Le résultat nest pas restitué, mais les flags sont mis à jour pour permettre les instructions de saut.JUMPJEJNEJGJGEJLJLEJUMP etiquetteJE etiquetteJNE etiquetteJG etiquetteJGE etiquetteJL etiquetteJLE etiquetteSauts conditionnels à létiquette donnée en paramètre. Laction dune instruction de saut dépend de la valeur des flags.INCDECINC regDEC regIncrémentation/Décrémentation de la valeur du registre spécifié. Les flags sont mis à jour.NOTNOT regNégation logique de la valeur du registre. Le résultat est stocké dans le registre. Les flags sont mis à jour.ANDORAND reg1, reg2OR reg1, reg2ET/OU logique des deux valeurs des registres. Le résultat est stocké dans le registre reg1, tandis que reg2 est inchangé. Les flags sont mis à jour.XCHGXCHG reg1, reg2Permute les contenus des deux registres.LIRELIRE regLIRE varLIRE tableau[reg]Lecture dune valeur et stockage dans le paramètre. La méthode de saisie nest pas définie au niveau du processeur.ECRIREECRIRE csteECRIRE regECRIRE varECRIRE tableau[reg]Ecriture de la valeur du paramètre. La méthode décriture nest pas définie au niveau du processeur.La gestion des entrées/sorties par le microprocesseur 8086 seffectue par le biais dinterruptions faisant intervenir dautres périphériques. Dans un souci de simplicité, nous avons remplacé ce mécanisme par deux instructions permettant les entrées/sorties de valeurs numériques.
Linstruction LIRE réalise une demande dune valeur numérique depuis lémulateur, tandis que linstruction ECRIRE réalise une écriture dune valeur numérique. Ces opérations dentrée/sortie ne sont pas gérées directement au niveau du processeur mais déléguées à lémulateur.
A titre dexemple, voici un programme calculant une moyenne de 5 valeurs.
; exemple : calcul de moyenne; déclaration des donnéesDATA Somme DW 0 Moyenne DW 0 Maximum DW 5 Notes DW[5]; instructions à exécuterCODEstart: MOV AX, 0 MOV BX, 0loop: LIRE Notes[BX] ADD AX, Notes[BX] INC BX CMP BX, Maximum JNE loop MOV Somme, AX DIV BX MOV Moyenne, AX ECRIRE Moyenne
Ce programme consiste à demander à lutilisateur dentrer cinq valeurs, qui vont être stockées dans le tableau « Notes ». La somme de ces valeurs est placée dans la variable « Somme ». Enfin, la moyenne est calculée par division entière, puis est stockée dans la variable « Moyenne ». Ce résultat est renvoyée en sortie.
Ce court programme illustre le format général dun fichier programme. On notera lordre des deux blocs introduits par DATA et CODE, ainsi que la déclaration de commentaires et détiquettes.
2. Modélisation en classes
Le langage de programmation retenu pour ce projet étant Java, la modélisation a été effectuée sous la forme dun arbre de classes, afin dutiliser au mieux les possibilités offertes dans ce domaine par ce langage, telles que lhéritage et le polymorphisme.
Chaque concept de lémulateur, comme celui de variable, de tableau ,de pile, de programme, de processeur, de mémoire, a été modélisé à laide de classes. Lémulateur lui-même est représenté sous la forme dune classe.
2.1. Modélisation des données générales
Larbre de classe utilisé pour la représentation des données (variables, registres, constantes, étiquettes) de base est montré ci-contre :
Afin de stocker diverses données comme des variables et des tableaux, nous avons besoin dune classe « Donnee » permettant de stocker une valeur numérique. Cette valeur est en lecture seule. Cette classe est à la base dautres classes permettant de stocker une étiquette (une donnée en lecture seule associée à un nom), une variable ou un registre (tous deux des données en lecture/écriture et associées à un nom).
La classe « Variable » sert à modéliser une variable ou un registre. Pour distinguer les deux utilisations, un champ « sorte » a été introduit, ainsi que deux variables de classe permettant de fixer la sorte (variable ou registre) dun objet de la classe Variable lors de sa construction.
La construction dune variable se fait à partir dun nom, dune valeur initiale et dune sorte. La valeur transmise est stockée dans le champ « valeurInitiale » qui peut être recopié dans le champ « valeur » (qui stocke la valeur courante) par un appel à la méthode « reinitialiser ».
Les tableaux sont modélisés par une classe « Tableau » indépendante des précédentes en termes dhéritage. Voici le schémas descriptif de cette classe :
Un tableau est la donnée dun nom et dun nombre fixe de cellules. Chaque cellule étant modifiable indépendamment des autres, on utilise la classe « Variable » pour modéliser une cellule. Le nom dune cellule est constitué du nom du tableau suivi du numéro de la cellule entre crochets.
La construction dun tableau se fait à partir dun nom et dune taille (un nombre de cellules).
La classe « ListeVariables » mentionnée ci-dessus est utilisée afin de modéliser une liste contenant un nombre quelconque de variables. Il sagit en fait dune classe étendant la classe Vector auquel on ajoute un attribut « Nom » accessible en lecture seule et fixé lors de la construction.
2.2. Modélisation des données internes du processeur
Le processeur manipule 3 types de données internes que sont les registres, les flags et la pile. Les registres sont modélisés par la classe « Variable », décrite précédemment. Les flags, quant à eux, sont modélisés par une classe « Flag », illustrée sur la page suivante :
Une instance de la classe « Flag » permet de contenir un booléen ainsi que les deux accesseurs. Notez quil ny a pas de constructeur particulier pour cette classe puisque le processeur est seul maître des valeurs des flags.
La pile est modélisée par une classe « Pile » étendant la classe « Stack » de Java. La pile ne contient que des instances de la classe « Donnee » : la valeur dune variable (ou dun registre) est placée dans la pile grâce à la méthode « getDonnee » de la classe « Variable ».
2.3. Modélisation de la mémoire
La mémoire est modélisée par une classe dont une instance est liée au processeur lors de sa construction (classe CPU décrite plus loin). Cette instance est également liée à un programme, stocké dans un vecteur de chaînes (« lignes ») et modélisé par la classe « Programme » décrite plus loin.
La mémoire contient une liste de variables, une liste de tableaux et une liste détiquettes. Ces trois éléments sont initialisés lors dun appel à « creerMemoire ». Cette méthode effectue une lecture détaillée des lignes du programme et traduit les déclarations de variables, de tableaux et détiquettes en instances des 3 classes correspondantes. Chaque instance est rangée dans une des 3 listes respectives.
La mémoire peut être réinitialisée aux valeurs de départ spécifiées dans le fichier source du programme par un appel à la méthode « reinitialiser ». Cette fonctionnalité évite une recréation de la mémoire à partir des lignes du programme à chaque nouvelle exécution. Cette réinitialisation sappuie sur la méthode « reinitialiser » de la classe « Donnee » ou de ses sous-classes.
Les trois méthodes « obtenirVariable », « obtenirTableau » et « obtenirEtiquette » servent pendant la traduction des lignes du programme en instructions exécutables, pour déterminer si une chaîne rencontrée est une variable, un registre, un tableau ou une étiquette valide.
Enfin, les deux accesseurs « getListeVariables » et « getListeTableaux » permettent à linterface un accès direct en lecture/écriture aux variables et aux tableaux.
2.4. Modélisation des instructions et des paramètres
Lors de la lecture dun fichier contenant un programme, chaque ligne est traduite en une instruction exécutable par le processeur. Celle-ci consiste en la donnée dun code dinstruction pris dans une liste de valeurs prédéfinies, dun numéro de ligne (qui fait office de numéro dinstruction) ainsi que dun booléen permettant de savoir si un point darrêt (valable uniquement pendant lexécution du programme) est placé sur cette instruction. Seules les véritables instructions ou les étiquettes peuvent être munies dun point darrêt.
Les trois classes utilisées pour modéliser les différentes sortes dinstructions (sans paramètre, unaire, binaire) sont représentées ci-contre. Les instances de ces classes sont crées au fur et à mesure de la traduction dun fichier source de programme et ce par la classe « Programme » décrite plus loin. Les instructions unaires et binaires ont besoin respectivement dune (de deux) instance(s) dune classe « Param » décrite plus bas. Cette classe abstraite est la classe de base de 4 sortes possibles de paramètres.
Afin de déterminer le code dinstruction dune ligne du programme, une liste de variable de classe a été placée dans la classe « Instruction » (52 codes possibles listés en annexe).
Lors de lexécution dun programme, le processeur utilisera directement ces instructions et les exécutera séquentiellement.
Les paramètres sont modélisés à laide de cinq classes, suivant leur sorte. Voici larbre de classes correspondant :
La classe abstraite « Param » définit la structure de base dun paramètre générique, à savoir son type (choisi dans les classes descendantes parmi une liste de variables de classes) et son accès possible ou non en écriture.
Un paramètre peut être :
une constante :Dans ce cas le paramètre est représenté par une instance de « ParamConstante ». La valeur est stockée dans le champ « valeur », accessible en lecture seule et fixée une fois pour toutes lors de la création de lobjet.
une variable (donc un registre ou une variable) :Dans ce cas le paramètre est représenté par une instance de « ParamVariable ». La variable est référencée par le champ « var » et est accessible en lecture. La possibilité décriture dépend de la valeur fixée pour le champ « readOnly » lors de la création de lobjet.
un tableau (indexé par un registre) :Dans ce cas le paramètre est représenté par une instance de « ParamTableau ». Le tableau est référencé par le champ « tab » et le registre qui lindexe par le champ « reg ». La cellule est accessible en lecture via les méthodes « getDonnee » (la seule utilisable lorsque le paramètre est en lecture seule) et « getElement » (écriture possible car cette méthode retourne une instance de Variable). Lécriture se fait de manière indirecte en obtenant la cellule par « getElement » et en modifiant sa valeur par sa méthode « setDonnee ».
une étiquette :Dans ce cas le paramètre est représenté par une instance de « ParamEtiquette ». Létiquette est référencée par le champ « etiq ». Une étiquette est accessible en lecture seule. Le numéro de ligne de létiquette peut être obtenu grâce à la méthode « getEtiquette ».
2.5. Modélisation dun programme
La classe « Programme » est responsable du stockage et de la traduction des instructions exécutables dun programme.
A chaque chargement dun nouveau programme, une instance de la classe « Memoire » est créée et construit la mémoire associée au programme à partir des lignes lues. Ensuite, une instance de la classe « Programme » est créée en passant en paramètre une référence au processeur et une référence à la mémoire précédemment créée. Une lecture détaillée des lignes du fichier source est alors effectuée afin den extraire les instructions sappuyant sur les données de la mémoire.
La lecture se fait ligne par ligne lors dun appel à « creerProgramme », et ce à partir du vecteur de chaînes transmis en paramètre. Ce vecteur est stocké dans le champ « lignes » pour permettre son affichage ultérieur dans linterface. La lecture dune ligne est déléguée à la méthode « traduireLigne », qui, grâce à la classe « StringTokenizer » de Java, découpe la ligne en mots (« tokens »). Suivant linstruction détectée, il y aura éventuellement un ou deux appels à la méthode « traduireParam » afin de traduire les paramètres en instances dune des sous-classes de « Param »
La méthode getInstruction, à laquelle on fournit un numéro de ligne, renvoie linstruction traduite à partir de cette ligne du fichier source. Cette méthode est utilisée par le processeur afin dobtenir les instructions à exécuter.
2.6. Modélisation du microprocesseur
La classe CPU est le cur de lémulateur puisquelle permet lexécution dinstructions à partir dune instance de la classe « Programme » et de la classe « Memoire ». Lémulateur possède une unique instance de cette classe. Les registres y sont représentés par un tableau dinstances de la classe « Variable » et les flags par un tableau dinstances de la classe « Flag ». La pile est également stockée comme uns instance de la classe Pile. Un ensemble de méthodes est fourni afin daccéder directement à chacun de ces éléments (« getPile », « obtenirRegistre » , « obtenirFlag ») Ainsi, cette classe est apte à manipuler tous les éléments du programme et va donc se charger de lexécution de ce dernier.
La méthode « getIP » permet à lémulateur de connaître le point dexécution en cours, ce qui est utile lors dune exécution en mode pas à pas ou lorsque celle-ci est suspendue suite à la rencontre dun point darrêt.
Lexécution est contrôlée par les quatre méthodes « init », « start », « run » et « stop », qui permettent de lancer lexécution selon plusieurs modes ( soit une exécution intégrale du programme, soit une exécution en pas à pas ou encore une exécution jusquà une ligne déterminée par lutilisateur). Il est également possible de relancer un programme qui a été au préalable arrêté. Le champ « executionEnCours » est mis à vrai lorsquune exécution est lancée et passe à faux dès quelle est stoppée, mais reste à vrai si elle nest que suspendue.
La méthode « init » réinitialise les contenus des registres, des flags, de la pile. De plus, la mémoire retrouve les valeurs qui lui avaient été affectées lors de sa création, cest-à-dire les valeurs extraites du fichier source du programme au moment de la lecture initiale. Le champ « executionEnCours » est aussi réinitialisé à faux.
La méthode « start » lance une exécution du programme à partir de sa première ligne. Pour cela, elle réinitialise le processeur par appel à la méthode « init », fixe « exécutionEnCours » à vrai et appelle la méthode « run » qui lance lexécution proprement dite. Le paramètre de cette méthode précise le mode dexécution et est directement transmis à la méthode « run ».
La méthode « run » fonctionne suivant trois modes distincts :
Le premier est lexécution intégrale du programme. Dans ce cas, « run » exécute le programme instruction par instruction, par appel à la méthode « runLigne » qui est chargée dexécuter une ligne. Les sauts et les appels de sous-programmes sont bien sûr respectés. Il ny a suspension de lexécution que pour les demandes dentrées/sorties et les points darrêt.
Le deuxième est lexécution en mode pas à pas. Ici, la méthode « run » exécute une seule et unique instruction non neutre (i.e. une véritable instruction du langage). Les sauts et les appels sont aussi respectés. Lintérêt de ce mode est la possibilité pour lutilisateur dexécuter le programme ligne par ligne depuis linterface, en vue de la recherche de bugs ou pour comprendre le fonctionnement des instructions et leur influence sur les registres, les données ou la pile.
Le troisième, à mi-chemin entre les deux précédents, permet de lancer lexécution jusquà une ligne précisée en paramètre.
La méthode « stop » interrompt brutalement lexécution du programme, en fixant « executionEnCours » à faux. Ainsi, « run » ne peut plus être appelée et une exécution ne pourra être reprise que par lintermédiaire de « start ».
3. Coordination du processeur, du programme et de la mémoire
Afin de coordonner le processeur avec la mémoire et un programme chargé, une classe « Machine » a été créée. Elle contient une instance de la classe « CPU » et crée une instance de la classe « Programme », ainsi quune instance de la classe « Memoire ».
Son rôle est double. Premièrement, elle assure la communication entre dune part les classes « CPU », « Programme » et « Memoire » et dautre part le reste de lapplication. Cela permet de sélectionner les actions autorisées sur le processeur depuis le reste de lapplication. Ainsi, une grande partie des méthodes de la classe « Machine » enveloppe celle de la classe « CPU » portant le même nom. Deuxièmement, cette classe soccupe de la lecture du fichier contenant le programme à exécuter et de la création dun vecteur de chaînes dont chaque élément est une ligne de ce fichier. Cette fonction est assurée par la méthode « chargerFichier ».
Le schéma suivant montre les relations entre les classes « CPU », « Memoire », « Programme », « Machine » et le reste de lapplication utilisant lémulateur : ce schéma indique clairement que toute communication entre dune part le processeur, la mémoire et le programme, et dautre part le reste de lapplication passe par la machine.
IV. Lapplication Emul8086
Lutilisation de lémulateur en mode texte nétant pas la finalité du projet (cette utilisation est cependant envisageable), nous avons développé une application sappuyant sur deux éléments que sont notre émulateur et une interface graphique. Le type dapplication retenu est un débogueur en mode graphique, utilisant pleinement les possibilités de lémulateur. Il est possible dans cette application de charger des programmes (un à la fois), de les exécuter et dinteragir avec la mémoire ainsi quavec le processeur pendant lexécution.
1. Premier aperçu de linterface graphique de lapplication
Cette interface se compose de 7 fenêtres ayant chacune une fonction bien précise :
une fenêtre principale proposant le menu des fonctions possibles telles que lexécution dun programme. Les six autres fenêtres ne sont utilisables quaprès le chargement dun fichier ;
une fenêtre permettant de visualiser le code du programme à exécuter, les lignes dotées dun point darrêt et la prochaine instruction à exécuter ;
une fenêtre permettant de visualiser les variables du programme et leurs valeurs. Il est possible de modifier la valeur dune variable pendant lexécution du programme ;
une fenêtre permettant de visualiser les tableaux du programme et leur contenu (liste des cellules et de leurs valeurs respectives) ;
une fenêtre permettant de visualiser les contenus des registres et des flags du processeur, également modifiables en cours dexécution ;
une fenêtre permettant de visualiser létat de la pile au point actuel dexécution ;
une fenêtre gardant la trace des entrées/sorties effectuées par les instructions LIRE et ECRIRE.
Pour illustrer ces propos, voici une capture décran montrant un programme en cours dexécution :
Dans la capture décran de la page précédente, le surlignage en rouge dans la fenêtre de « Code Source » signale un point darrêt sur linstruction présente sur cette ligne. Le surlignage en bleu, quant à lui, met en évidence la prochaine instruction qui sera exécutée.
2. Manuel de lutilisateur
La fenêtre principale, seule visible au démarrage de lapplication, comporte quatre menus : « Fichier », « Affichage », « Exécution » et « Aide ». Voici un montage qui donne une description rapide de laction effectuée par chaque commande de menu.
Il est à noter que seul le menu « Fichier » est actif au lancement de lapplication et quun programme doit être chargé pour activer les menus « Affichage » et « Exécution », dans le but déviter des incohérences telles que le déclenchement dune exécution sans quun programme nait été chargé au préalable.
Pour charger un fichier, utilisez la commande « Charger
» du menu fichier. Une boîte de dialogue souvre alors pour vous permettre den sélectionner un. Il ny a aucune restriction sur les noms de fichiers, mais celui qui est choisi doit contenir le code source dun programme, sans quoi une erreur de lecture se produira, si le fichier est mal formé du point vue syntaxique (instruction inconnue, nombre ou type de paramètre incorrect, absence des mots clés CODE ou DATA). La cohérence du code, ainsi que des données, ne sera vérifiée quau cours de lexécution. Une fois le fichier chargé, son contenu apparaît dans la fenêtre de « Code Source » et les autres fenêtres souvrent en vue de lexécution du programme.
Il est possible dexécuter un programme de diverses manières. La manière la plus simple consiste à lancer une exécution intégrale, par le raccourci clavier F9 depuis la fenêtre de « Code Source ». Ce mode nest pas interactif, excepté pour les demandes dentrées de données ou les points darrêt si le programme en contient.
Il existe deux manières de contrôler plus finement lexécution du programme. La première permet son exécution non interactive jusquà une ligne du code choisie par lutilisateur, et sa suspension lorsque cette ligne est atteinte. Pour ce faire, il suffit de se placer sur la ligne en question et dappuyer sur la touche F4. Cette fonctionnalité permet de savoir si une instruction est ou non exécutée, ou de contrôler facilement lexécution dune boucle.
La seconde, appelée mode « pas à pas », permet lexécution la plus détaillée, à savoir instruction par instruction. Pour lutiliser, employez le raccourci clavier F7. A chaque appui sur cette touche, une seule et unique instruction est exécutée. Il sagit du mode le plus pratique pour bien visualiser les modifications des registres, des flags, de la pile, des variables et des tableaux, suite à lexécution de chaque ligne du programme. En outre, il permet de visualiser lenchaînement des sauts et des appels de sous-programmes.
Les différents modes dexécution peuvent être utilisés conjointement pour, par exemple détailler lexécution dune petite partie dun programme.
Un autre moyen dobtenir plus de détails sur lexécution dun programme est lutilisation de points darrêt. Un point darrêt posé sur une instruction demande à ce que lexécution soit temporairement suspendue à chaque fois cette instruction est atteinte. Pour fixer un point darrêt sur une ligne, opération qui nest possible que sur une vraie instruction ou une étiquette, sélectionnez-la et utilisez le raccourci clavier F5. Sil est permis de poser un point darrêt sur cette ligne, elle sera surlignée en rouge. De la même manière, il est possible de supprimer un point darrêt existant, en le sélectionnant et en appuyant sur F5. A lexécution, lorsquun point darrêt est rencontré, lapplication rend la main à lutilisateur, qui peut au choix poursuivre lexécution par la touche F9 ou utiliser lun des deux autres modes (exécution jusquà une ligne précise ou pas à pas).
A tout moment, il est possible dinterrompre lexécution, grâce à lutilisation de la commande « Arrêter » , accessible depuis le raccourci clavier Contrôle + F2. On peut également relancer lexécution depuis le début du programme en utilisant la commande « Relancer », avec le raccourci Ctrl + F9.
Lorsque lexécution est suspendue (et non pas terminée), cest à dire lorsque la fenêtre fait apparaître un point dexécution, les contenus des variables, des tableaux, des flags et des registres autorisés (AX, BX, CX et DX uniquement) sont modifiables à volonté. Pour cela, utilisez la zone dédition correspondante à lélément dont on veut modifier la valeur (qui doit impérativement être comprise entre 0 et 65535). Saisissez la nouvelle valeur, puis appuyez sur entrée pour confirmer la modification. Si la valeur saisie est erronée, un message derreur saffiche et la modification nest pas prise en compte. Notez quil est impossible depuis lapplication de rajouter ou de supprimer une variable ou un tableau.
Pour ce qui est des flags, il suffit de cocher ou de décocher la case correspondante pour changer leur état (la case cochée signifie que le flag est à « vrai »).
Certains programmes peuvent faire intervenir des entrées/sorties, au moyen des instructions LIRE et ECRIRE. Le résultat dune instruction ECRIRE est laffichage dun message dans la fenêtre d « Entrées/Sorties », comportant la valeur à afficher ainsi que sa provenance. Linstruction LIRE, quant à elle, provoque laffichage dune boîte de dialogue demandant dentrer une valeur pour lélément donné en paramètre de la dite instruction, dont un exemple est montré ci-contre.
Une fois la valeur saisie, lexécution continue et une trace de cette opération apparaît dans la fenêtre d « Entrées/Sortie ». Il est à noter, au risque de se répéter, que les valeurs à saisir doivent toujours être comprises entre 0 et 65535. Si tel nest pas le cas, la demande de valeur de renouvelée jusquà lobtention dune valeur correcte. Lutilisateur peut aussi choisir dannuler la saisie de la valeur. Dans ce cas, lexécution est interrompue.
3. Fonctionnement interne de lapplication
Lapplication se compose dune classe principale, appelée « Emul8086 » qui gère à la fois la partie interface graphique et la partie émulateur. Le détail du fonctionnement de lémulateur a déjà été abordé dans ce rapport. Nous allons donc nous limiter à décrire le fonctionnement de linterface graphique, ainsi que la communication entre linterface graphique et lémulateur.
La classe « Machine », qui dans un premier temps était indépendante, a été adaptée de façon à permettre son pilotage par lapplication. Au démarrage de celle-ci, une instance de la classe « Emul8086 » est créée. Dorénavant, le terme application désignera cette instance. Elle crée à son tour une instance de la classe « Machine » et met en place la connexion avec lémulateur. De plus, linterface graphique est initialisée.
Linterface graphique fonctionne à laide de sept classes, définissant chacune lapparence et le fonctionnement dune des fenêtres de linterface (précédemment décrites). Voici, une table de correspondance entre le nom de la fenêtre et le nom de la classe qui lui est associée :
Nom de la fenêtreNom de la classePrincipaleTFenPrincVariablesTFenEditeurVarTableauxTFenEditeurTabRegistres et FlagsTFenEditeurRegCode SourceTFenCodeSourcePileTFenEditeurPileEntrées/ SortiesTFenEntreeSortieLors du chargement dun fichier, depuis le menu de la fenêtre principale, lapplication reçoit lordre de charger le fichier indiqué par lutilisateur. Ce chargement seffectue en trois étapes. La première consiste à vider linterface, au cas où un autre programme aurait été chargé précédemment. La deuxième transfère cette requête à lémulateur (i.e. à linstance de la classe « Machine »). La troisième met à jour linterface à partir des données traduites et mises à disposition par lémulateur. Cette remise à jour de linterface comprend, entre autre, létablissement dune liaison entre dune part les différentes fenêtres de linterface et dautre part les données rendues disponibles par lémulateur.
Les diverses commandes dexécution disponibles depuis linterface graphique sont transmises à lapplication par la fenêtre principale ou la fenêtre de Code Source (via les raccourcis clavier mentionnés précédemment). Lapplication transmet, à son tour, ces commandes à lémulateur, qui effectue leur traitement et retourne les résultats. Ces derniers sont alors récupérés par lapplication, qui met à jour linterface graphique en réponse aux changements de données fournis par lémulateur, à savoir les valeurs des variables, des registres, des flags, de la pile, du point dexécution et des cellules des tableaux. Grâce à ce système, il y a indépendance de linterface graphique vis à vis de lémulateur et toute communication entre ces deux sous-ensembles de lapplication passe obligatoirement par cette dernière. Il y a donc un échange permanent dinformation entre lémulateur et linterface, comme indiqué sur le schéma suivant :
Lutilisateur peut également interagir avec lémulateur, en modifiant des variables, des registres, des flags et en basculant des points darrêt.
Lorsque lutilisateur valide une modification de la valeur dune donnée depuis linterface, cette demande est prise en compte directement dans linstance de la classe « Variable » conservant la valeur de cette donnée. Si la valeur saisie est incorrecte, la valeur initiale est rétablie dans la zone dédition de la donnée. Pour les flags, la modification est prise en compte directement par linstance correspondante de la classe « Flag ».
Le fait de basculer un point darrêt depuis la fenêtre de code source transmet la requête à lapplication, qui la transmet à son tour à lémulateur. Ce dernier accepte le point darrêt si la ligne sur laquelle il doit être placé contient une instruction ou une étiquette. Lémulateur refuse de placer un point darrêt sur toute autre ligne du fichier source.
Lors de lexécution dune instruction dentrée (« LIRE ») ou de sortie (« ECRIRE »), il y a également un va-et-vient entre linterface graphique et lémulateur.
Lors dune lecture, lémulateur demande la saisie dune valeur à lapplication. Celle-ci transmet la demande à la fenêtre gérant les entrées/sorties, qui ouvre une boite de dialogue qui spécifie lélément pour lequel on désire une valeur et qui invite lutilisateur à entrer celle-ci. Le fait dannuler cette saisie stoppe lexécution. Si la saisie est validée, le résultat est retourné à lapplication qui le transmet à son tour à lémulateur. Celui-ci peut alors poursuivre lexécution après avoir mémorisé cette nouvelle valeur.
Lors dune écriture, lémulateur transmet lordre de sortie à lapplication qui transmet cet ordre à la fenêtre gérant les entrées/sorties. Cette dernière effectue simplement un affichage du nom de lélément à afficher ainsi que de sa valeur.
V. Conclusion
Lapprentissage de la programmation en assembleur étant une tâche ardue pour un programmeur, nous avons tenté, au travers de cette application, de la lui faciliter. Son interface conviviale, mettant laccent sur les bases en laissant les détails trop techniques de coté, a pour ambition daider le débutant à mieux appréhender le fonctionnement de lassembleur.
Du fait de linteractivité existante pendant lexécution dun programme, il est particulièrement aisé de comprendre les effets de chaque instruction non seulement sur lordre dexécution de celles-ci, mais aussi sur les données manipulées (mémoire, pile, registres et flags).
En ce qui nous concerne, ce projet a été loccasion de développer une véritable application écrite en Java, et ce en utilisant ses capacités en terme de gestion dinterfaces graphiques. Le fait de programmer dans un langage objets nous a permis dapprendre à modéliser entièrement un problème à laide dune hiérarchie de classes. Dautre part, le fait que le stage soit en rapport étroit avec la programmation en assembleur et le fonctionnement dun microprocesseur nous a apporté une meilleure maîtrise de ces deux notions.
Bibliographie
James W.COFFRON, « 8086/8088 fonctionnement et programmation », éd. Micro Passion Collection, 1983.
Steven GUTZ, « Up to speed with Swing user interfaces with Java foundation classes », éd. Manning, 1998.
Mark C.CHAN, Steven W.GRIFFITH, Anthony F.IASI, « 1001 Java Programmers Tips », éd. Jamsa Press, 1997.
Sun, Java 2 Software Development Kit SE v1.3 : HYPERLINK "http://java.sun.com/j2se/1.3/docs/index.html" http://java.sun.com/j2se/1.3/docs/index.html.
Sun, The Java Tutorial : HYPERLINK "http://java.sun.com/docs/books/tutorial/index.html" http://java.sun.com/docs/books/tutorial/index.html.
Annexe A : Liste des codes dopérations
Cette liste est extraite du fichier « Instruction.java » et reprend simplement les déclarations de variables de classe.
public static final int OPER_NOP = 0;
public static final int OPER_PUSH_REG = 1;
public static final int OPER_POP_REG = 2;
public static final int OPER_MOV_REG_REG = 3;
public static final int OPER_MOV_REG_VAR = 4;
public static final int OPER_MOV_VAR_REG = 5;
public static final int OPER_MOV_REG_CST = 6;
public static final int OPER_MOV_REG_TAB = 7;
public static final int OPER_MOV_TAB_REG = 8;
public static final int OPER_CALL_ETIQ = 9;
public static final int OPER_RET = 10;
public static final int OPER_ADD_REG_CST = 11;
public static final int OPER_ADD_REG_REG = 12;
public static final int OPER_ADD_REG_VAR = 13;
public static final int OPER_ADD_REG_TAB = 14;
public static final int OPER_SUB_REG_CST = 15;
public static final int OPER_SUB_REG_REG = 16;
public static final int OPER_SUB_REG_VAR = 17;
public static final int OPER_SUB_REG_TAB = 18;
public static final int OPER_MUL_CST = 19;
public static final int OPER_MUL_REG = 20;
public static final int OPER_MUL_VAR = 21;
public static final int OPER_MUL_TAB = 22;
public static final int OPER_DIV_CST = 23;
public static final int OPER_DIV_REG = 24;
public static final int OPER_DIV_VAR = 25;
public static final int OPER_DIV_TAB = 26;
public static final int OPER_CMP_REG_CST = 27;
public static final int OPER_CMP_REG_REG = 28;
public static final int OPER_CMP_REG_VAR = 29;
public static final int OPER_CMP_REG_TAB = 30;
public static final int OPER_JUMP_ETIQ = 31;
public static final int OPER_JG_ETIQ = 32;
public static final int OPER_JGE_ETIQ = 33;
public static final int OPER_JE_ETIQ = 34;
public static final int OPER_JNE_ETIQ = 35;
public static final int OPER_JL_ETIQ = 36;
public static final int OPER_JLE_ETIQ = 37;
public static final int OPER_INC_REG = 38;
public static final int OPER_DEC_REG = 39;
public static final int OPER_NOT_REG = 40;
public static final int OPER_AND_REG_REG = 41;
public static final int OPER_OR_REG_REG = 42;
public static final int OPER_XCHG_REG_REG = 43;
public static final int OPER_LIRE_REG = 44;
public static final int OPER_LIRE_VAR = 45;
public static final int OPER_LIRE_TAB = 46;
public static final int OPER_ECRIRE_CST = 47;
public static final int OPER_ECRIRE_REG = 48;
public static final int OPER_ECRIRE_VAR = 49;
public static final int OPER_ECRIRE_TAB = 50;
public static final int OPER_ETIQ = 51;
Table des matières
TOC \o "1-3" \h \z HYPERLINK \l "_Toc518022603" I. Présentation du sujet PAGEREF _Toc518022603 \h 1
HYPERLINK \l "_Toc518022604" II. Rappels sur le microprocesseur Intel 8086 PAGEREF _Toc518022604 \h 2
HYPERLINK \l "_Toc518022605" III. Modélisation de lémulateur PAGEREF _Toc518022605 \h 4
HYPERLINK \l "_Toc518022606" 1. Caractéristiques générales PAGEREF _Toc518022606 \h 4
HYPERLINK \l "_Toc518022607" 2. Modélisation en classes PAGEREF _Toc518022607 \h 6
HYPERLINK \l "_Toc518022608" 2.1. Modélisation des données générales PAGEREF _Toc518022608 \h 7
HYPERLINK \l "_Toc518022609" 2.2. Modélisation des données internes du processeur PAGEREF _Toc518022609 \h 8
HYPERLINK \l "_Toc518022610" 2.3. Modélisation de la mémoire PAGEREF _Toc518022610 \h 8
HYPERLINK \l "_Toc518022611" 2.4. Modélisation des instructions et des paramètres PAGEREF _Toc518022611 \h 8
HYPERLINK \l "_Toc518022612" 2.5. Modélisation dun programme PAGEREF _Toc518022612 \h 10
HYPERLINK \l "_Toc518022613" 2.6. Modélisation du microprocesseur PAGEREF _Toc518022613 \h 11
HYPERLINK \l "_Toc518022614" 3. Coordination du processeur, du programme et de la mémoire PAGEREF _Toc518022614 \h 12
HYPERLINK \l "_Toc518022615" IV. Lapplication Emul8086 PAGEREF _Toc518022615 \h 13
HYPERLINK \l "_Toc518022616" 1. Premier aperçu de linterface graphique de lapplication PAGEREF _Toc518022616 \h 13
HYPERLINK \l "_Toc518022617" 2. Manuel de lutilisateur PAGEREF _Toc518022617 \h 14
HYPERLINK \l "_Toc518022618" 3. Fonctionnement interne de lapplication PAGEREF _Toc518022618 \h 16
HYPERLINK \l "_Toc518022619" V. Conclusion PAGEREF _Toc518022619 \h 18
HYPERLINK \l "_Toc518022620" Bibliographie PAGEREF _Toc518022620 \h 19
HYPERLINK \l "_Toc518022621" Annexe A : Liste des codes dopérations PAGEREF _Toc518022621 \h 20
Laboratoire dEtudes et de Recherche en Informatique dAngers
LERIA
Emulation graphiquedu fonctionnementdun microprocesseur 8086
Frédéric BEAULIEU
Yan LE CAM
Responsable du projet : Mr. Jean-Michel RICHER
2000-2001
Université dAngers
Département Informatique
Emulation graphique du fonctionnement dun microprocesseur 8086
Page PAGE 20 / 20