Académique Documents
Professionnel Documents
Culture Documents
1. Introduction
Un CPU exécute des instructions qui sont la résultante des programmes écrits avec des langages de
programmation. Ces instructions contenues dans la mémoire composent le langage d’assemblage
(assembleur). Pour qu’un programme fonctionne, le CPU lit une part une les instructions. Donc
pour les atteindre, les instructions sont rangées dans la mémoire via des adresses. Au chargement
d’un programme, le CPU dispose de l’adresse d’origine et contient dans son Compteur Ordinal
l’adresse de la future instruction à exécuter.
Les instructions disposent de toutes les données pour que combinées entre elles et exécutées sur le
CPU l’algorithme original traduit en langage de programmation fonctionne. Les données des
instructions se retrouveront soit dans les registres du CPU soit dans la mémoire centrale.
Il existe 2 familles d’architecture de CPU, les CISC (Complex Instruction Set Computer) et
les RISC (Reduced Instruction Set Computer). Leurs différences résident dans la construction des
instructions. Un ensemble d’instructions est appelé jeux d’instructions ou ISA (Instruction Set
Architecture).
• Le CISC dispose de jeux d’instructions pouvant effectuer plusieurs opérations, comme les
opérations arithmétiques, les opérations logiques, le chargement et la récupération de
données en mémoire. Ils sont présents dans les x86, les pentiums, d’Intel, les 68xx de
Motorola.
• Le RISC dispose d’instructions relativement petites, unitaires, codées sur les mêmes tailles
de mot avec une exécution cadencée sur les mêmes cycles d’horloge. Les instructions RISC
sont possibles grâce à ses nombreux registres, caches (et aux mémoires) ne limitant pas la
taille des demandes. Les puces RISC sont conçues pour exécuter ces instructions très
rapidement.
lignée de micro-processeur INTEL), de comprendre l’utilité des différents registres de son CPU et
de découvrir les bases du langage assembleur 8086 au travers de l’émulateur EMU 8086.
2. Le microprocesseur 80x86
Le microprocesseur 80x86o (voir figure 1) se présente sous la forme d'un boîtier DIP (Dual Inline
Package). En électronique un DIP est un boitier contenant des circuits intégrés qui dispose de
sorties pour se connecter à un environnement externe (de nouveau un circuit intégré). Ce processeur
a un bus d'adresses et un bus de données multiplexés (certaines pattes transmettent à certains
moments un bit d'adresse et à d'autres moments un bit de donnée). Il est organisé autour d’un bus
interne de données de 16 bits, il comporte:
• 16 broches pour transporter les données (AD0 ... AD15).
• 20 broches pour véhiculer les adresses (A0 ... A19). Il peut adresser 220 (= 1048576)
positions mémoire différentes contenant chacune 1 octet (8 bits) : la mémoire a au plus une
capacité de 1 Mo.
• de registres, 16 bits pour stocker des données.
• un bloc pour former des adresses, 20 bits à partir de morceaux d’adresse codés sur 16 bits.
Comme tout CPU, le 8086 dispose d'un certain nombre de type de registres:
• les registres généraux destinés au traitement des valeurs. Ils permettent d’effectuer des
additions, des multiplications, des calculs logiques, nommés A (Accumulateur), B (Base), C
(Compteur), D (Donnée).
• les registres d'adressages (registres de segments, pointeurs et index) utilisés pour pointer,
lire ou écrire un endroit en mémoire, nommés D (Donnée), S (Stack - Pile), C (Code), E
(Extra). L’IP (Instruction Pointer) sera aussi nommé CO (Compteur Ordinal).
Les flags du registre d’état de la figure ci-dessus contiendront alors les valeurs mis à 0 ou 1 par le
processeur (à la suite de l'exécution d'une instruction) ou par le programmeur pour modifier le mode
de fonctionnement du processeur.
La gestion des registres est faite avec des instructions processeurs. Ces instructions sont formées
avec le code binaire des programmes à exécuter. Il est possible de programmer le processeur avec
des langages de plus bas niveau et pour rendre plus facile la manipulation, la lecture et l’écriture de
telles instructions binaires, le programmeur utilise des symboles (mots clés, ou mnémoniques) à la
place de des codes binaires et fait appel à l’assembleur. Les symboles en assembleur sont souvent la
compression d'un mot ou d'une expression réalisant une action. Par exemple MOV (MOVe), MUL
(MULtiply), … . Les mnémoniques sont suivies de registres, de valeurs, … .
Si l'on manipule des nombres codés sur 16 bits (1 mot mémoire), 4 registres généraux sont utilisés :
• AX, l‘Accumulateur, utilisé pour stocker les résultats de certains calculs arithmétiques et
logiques.
• BX, la Base, utilisé comme registre de base pour des données dans le segment pointé par le
registre de segment ES. Un segment est un ensemble d’octets consécutifs dont un octet
désigne le couple (numéro du segment, déplacement dans le segment).
• CX, le Compteur, utilisé pour les boucles.
• DX, le registre de Données, contient l'adresse des ports d'entrée/sortie (pour les instructions
IN et OUT) et sert également d'extension à AX pour manipuler les données sur 32 bits.
Si l'on utilise des nombres codés sur 8 bits (1 octet), les 4 registres généraux en mode 8 bits sont
utilisés pour en produire 8 registres AH, AL, BH, BL, CH, CL, DH et DL avec comme convention,
H est mis pour "High" et L pour "Low" :
• AH contient l'octet de poids fort du registre AX.
• BH contient l'octet de poids fort du registre BX.
• ….
• AL contient l'octet de poids faible du registre AX.
• BL contient l'octet de poids faible du registre BX.
• ….
Les 2 registres d'index, notés SI (Source Index) et DI (Destination Index), sont utilisés pour indexer
les éléments d'un tableau.
Une instruction dans le CPU est composée de plusieurs segments. Un segment qui désigne le code
du programme, un segment qui désigne les données, un segment qui désigne l’organisation des
appels, ….. Une instruction contient donc tous ses segments, mais sous forme d’adresse pointant
vers la mémoire. Pour accéder à sa mémoire centrale, le 80x86 dispose de registres de segment
suivants:
• CS (Code Segment) pointe sur la base du segment qui contient le code (les instructions que
doit exécuter le processeur).
• DS (Data Segment) pointe sur la base du segment contenant les données (variables, tableaux
...).
• SS (Stack Segment) pointe sur la base du segment qui contient la pile gérée par les registres
SP et BP.
Computer Systems – ICT1, tiré du cours de Pr. Cyril-Alexandre PACHON
5
• ES (Extra Segment) pointe sur la base d'un segment supplémentaire qui est généralement
utilisé pour compléter le segment de données.
Ces segments sont situés selon la place disponible dans la mémoire. Parfois, ces segments peuvent
se chevaucher partiellement. Les adresses des segments sont données et gérées par le système
d’exploitation lors du mécanisme de chargement du programme à exécuter. Comme les adresses
sont ensuite contrôlées de façon automatique, normalement il n’y a pas de risque de confusions
d’accès (des systèmes de gestion d’erreurs peuvent se mettre en place comme les blue Sreen). Par
contre, les déplacements sont liés aux instructions contenues dans le programme en se base sur une
adresse d’origine.
La pile est une zone de mémoire qui permet de conserver de manière temporaire des données (par
exemple, l’état des registres lors d’un appel de procédure). Nous utilisons 2 registres pointeurs :
• SP (Stack Pointer) pointe sur le sommet de la pile et se met à jour automatiquement par les
instructions d'empilement et de dépilement.
• BP (Base Pointer) pointe la base de la région de la pile contenant les données accessibles
(variables locales, paramètres,...) à l'intérieur d'une procédure. Il doit être mis à jour par le
programmeur.
La pile n'est pas gérée avec des registres, elle utilise la mémoire (RAM). Dans cette pile, les
données sont naturellement stables et seuls les pointeurs SP et BP sont utilisés pour atteindre les
données. Cette pile est de type LIFO et elle est manipulée par :
• PUSH pour empiler une valeur de 16 bits (le pointeur SP est décrémenté automatiquement
de 2).
• POP pour dépiler une valeur de 16 bits (SP est incrémenté automatiquement de 2).
Le bus d’adresse de la mémoire réelle est de 20 bits. Or le 80x86 n’a pas de registre de 20 bits mais
que de 16 bits. Un registre de 16 bits référence 64Ko de mémoire, soit 216 = 65 536. Pour répondre
à cette problématique le 80x86 en mode réel combine un des neuf registres généraux avec l'un des
quatre registres de segments pour former une adresse de 20 bits.
Le registre IP (Instruction pointer) est appelé pointeur d'instruction ou compteur ordinal. La valeur
contenue dans ce registre aiguille instruction par instruction les déplacements le micro-processeur.
Il propose toujours la prochaine adresse de l’instruction à exécuter par le micro-processeur. Le
registre IP est constamment modifié après chaque fin instruction pour qu'il puisse pointer sur
l'instruction suivante. Ce registre permet de pointer une case mémoire dans le segment de code afin
que le 80x86 puisse charger la prochaine instruction à exécuter.
À chaque type d'accès en mémoire, il faut faire correspondre un registre de segment et parfois un
registre général, par exemple :
Pour adresser 1 méga-octet, il faut 4 bits, en plus des 16 bits d'un registre général. Cette
combinaison est obtenue en décalant le registre de segment de 4 bits et en ajoutant le résultat au
contenu du registre général pour obtenir l'adresse sur 20 bits.
Le résultat est appelé EA (Effective Address). L'EA est placée sur le bus d'adresses afin de pouvoir
accéder à l'emplacement mémoire correspondant. L’EA est constituée de deux parties, le registre de
segment définissant une zone mémoire de 64Ko, et le registre général spécifiant un déplacement à
partir de l'origine de ce segment de 64Ko (c'est-à-dire une adresse sur 16 bits à l'intérieur de ce
segment).
L’EA est exprimée en donnant le registre segment et le registre général séparés par deux points par
exemple: DS:SI. Le registre DS est le segment de 64Ko et SI contient le déplacement dans ce
segment. Si nous avons par exemple 1A8B:0010, l'adresse du segment est 1A8B et le déplacement
dans le segment est 0010 en hexadécimal. Pour obtenir l’EA il faut faire : 1A8B * 10 16 + 10 =
1A8C0.
Opérations logiques
Le 8086 permet d’effectuer des opérations binaires classiques grâce au mnémoniques AND, OR,
NOT et XOR. Ces opérateurs peuvent être utilisés avec :
Exemple:
AND AL, 11000000b ; résultat 01000001b
OR AL, 01110110b; résultat 01110111b
NOT AL; résultat 10001000b
XOR AL, 01010101b; résultat 11011101b
XOR BX, BX ; résultat 00000000b
Le 8086 permet d’effectuer des opérations de décalage sur des nombres signés ou non signés. Les
bits qui sont expulsés sont stockés dans le bit CF :
Les mnémoniques ADD (ADDition) et SUB (SUBtraction) permettent d’effectuer des additions et
des soustractions. Ils sont construit par :
• ADD registre, registre ou variable
• ADD registre ou variable, registre
• ADD registre ou variable, constante
• SUB registre, registre ou variable
• SUB registre ou variable, registre
• SUB registre ou variable, constante
Exemple:
ADD AX, BX; effectur l'opération Ax=AX+BX
SUB AX, BX; effectue l'opération AX = AX - BX
Si une addition ou une soustraction sont codées sur 16 bits, alors leurs résultats seront codés sur au
plus 16 + 1 bits soit 16 bits de résultat + 1 bit de retenue (le bit CF). Les opérations ADD et SUB
sont effectuées sans tenir compte de l’état initial du bit CF du registre d’état mais l’état de ce bit
peut être modifié à l’issue de l’opération.
Pour demander au processeur d’effectuer des additions qui tiennent compte du bit CF, nous devons
utiliser un nouveau mnémonique : ADC (ADdition with Carry).
Il n’existe pas de mnémonique SUBC mais un mnémonique appelé SBB (SuBtract with Borrow)
qui effectue le même type d’opération mais en sens inverse.
Comme les opérations ADC et SBB modifient l’état du bit CF, il sert à effectuer des opérations sur
48 bits, 64 bits … en stockant les résultats intermédiaires en mémoire, l’opération se fera par bloc
de 16 bits.
Le mnémonique MUL (MULtiply) permet les multiplications, mais son mode de fonctionnement est
très différent par rapport à celui de l'addition et la soustraction.
• MUL registre ou variable.
La multiplication MUL avec deux données de 8 bits donne comme résultat une valeur codée sur 16
bits, donc de façon directe les valeurs données ne peuvent pas dépasser les bits de poids faibles. Si
les valeurs dépassent les 8 bits, il faut alors utiliser plusieurs registres. Pour multiplier un nombre N
par un nombre M, nous devons d'abord copier le nombre N dans le registre AL (ou AX) puis
invoquer l’instruction MUL M :
• si l’opérande est un octet, nous calculons AL * opérande et le résultat est stocké dans AX.
• si l’opérande est un mot, nous calculons AX * opérande et le résultat est stocké dans les
registres DX et AX (DX contenant la partie de poids fort).
Le 8086 propose un autre mnémonique pour effectuer les multiplications entre 2 nombres signés.
Le mnémonique IMUL est utilisé de la même manière que le mnémonique MUL à la différence que
les opérandes sont considérés comme des nombres signés (codés en complément à 2) et que le
résultat est un nombre signé.
Le mnémonique de la division euclidienne est DIV et son mode de fonctionnement est voisin de
celui de MUL.
Pour diviser un nombre N par un nombre M, nous devons d'abord copier le nombre N dans le
registre AX (ou AX et DX) puis invoquer l’instruction DIV M :
• si l’opérande est un octet, nous calculons AX / opérande : le quotient est stocké dans AL et le
reste dans AH.
• dans le cas d’un mot, nous calculons (AX DX) / opérande : le quotient est stocké dans AX et
le reste dans DX.
Le 8086 propose un autre mnémonique pour effectuer les divisions euclidiennes entre 2 nombres
signés. Le mnémonique IDIV est utilisé de la même manière que le mnémonique DIV. Les
différences sont que les opérandes sont considérées comme des nombres signés (codés en
complément à 2) et que le résultat est un nombre signé.
Les contrôles
Les mnémoniques STD et CLD sont utiles pour aiguiller le sens de manipulation des chaines
d’octets. Si le flag DF est à 0, le traitement est de gauche à droite, si DF est à 1 le traitement va de
droite à gauche.
4. Les variables
Comme pour tous les langages, avant d’utiliser une variable, il faut initialement la déclarer. Les
variables sont considérées comme des emplacements mémoire qui peuvent être manipulés par
l’intermédiaire de leur adresse ou de leur nom.
Le nom d’une variable peut être une combinaison de chiffres, de lettres et du caractère « _ », mais il
doit respecter les contraintes suivantes :
L'association entre le nom et l'adresse s'effectue en utilisant les instructions DB (Define Byte – 1
octet pour 8086) ou DW (Define Word – 2 octets pour 8086) selon que la variable contient des
octets ou des mots.
var1 DB 50h
var2 DB?
var3 DW 1110h
Il peut être intéressant de connaître l’adresse d’une variable pour utiliser cette dernière comme
argument des interruptions logicielles. Celle-ci se décompose en 2 parties :
1. SEG nom remplacé par le nom du registre associé au segment contenant la donnée.
2. OFFSET nom remplacé par la valeur du déplacement.
var1 DB 50h
Un tableau peut être vu comme une liste de variables qui sont associées à un même nom. La
création d’un tel tableau s’effectue alors de la manière suivante :
tab DB 01h, 02h, 03h
Il est alors possible d’accéder à une case précise du tableau en utilisant un opérateur d’indexation :
nous écrivons le nom du tableau suivi du numéro de la case placé entre crochet (les cases étant
numérotées de 0 à N-1 pour un tableau comportant N éléments) :
Une chaîne de caractères peut être vue comme un tableau contenant une suite de code ASCII
séparés par une virgule (correspondant chacun à une lettre). Dans tous les cas, et pour faciliter les
parcours et les recherches, les chaines de caractères seront terminées par le symbole $ (pour
marquer la fin de la chaîne de caractères).
Cette écriture est fastidieuse car il faut connaître le code ASCII de chaque caractère. Nous pouvons
opter pour une séquence dissociée de caractères notés chacun entre symboles ' ' et séparés par une
virgule.
5. Instructions de saut
Les étiquettes (nommées aussi labels) permettent de spécifier au programme à quel endroit
continuer une exécution. Il s’agit d’une ancre de programmation, comme un point d’entrée pour
commencer des instructions. Pour accéder à une étiquette, il est possible d’utiliser un mnémonique
de contrôle du flot d'instructions : JMP (pour jump). Nous parlons ainsi de programmation par Go
To non conditionnelle. Le branchement inconditionnel consiste simplement à sauter d'une position
dans le code à une autre position pour continuer l'exécution. Ce type de branchement est opposé aux
branchements conditionnels qui réalisent des sauts en fonction des tests effectués sur les bits du
registre d'état. Le saut inconditionnel est JMP suivi d'un nom de l'étiquette (ou d'un nombre
représentant l'adresse de destination codée sur 4 octets mais ce n'est pas conseillé). Pour que les
instructions s’exécutent, l’étiquette atteinte est convertie en une adresse au moment de l'assemblage
des instructions de saut.
Pour ajouter une étiquette dans le programme assembleur, il suffit de déclarer un nom (qui ne
commence pas par un chiffre) suivi des 2 points ( : ).
Par exemple :
etiquette1: MOV AX, BX
JMP etiquette1
A l'instar de l'instruction JMP, les instructions de branchement conditionnel s'utilisent avec un label.
Elles doivent être placées juste après l'instruction (CMP, DEC, INC ...) qui modifient l'état du
registre d'état sinon les autres mnémoniques placées entre cette instruction et le branchement
conditionnel pourraient altérer les bits du registre d'état. L'instruction CMP (pour compare) affecte
les drapeaux du registre d'état (instructions de branchement conditionnel afin d'implanter des
boucles, des tests ...). La comparaison de 2 nombres s'effectue en faisant une pseudo-soustraction
qui affecte les drapeaux dans le registre d’état :
• OF : Overflow Flag
• SF : Sign Flag
• ZF : Zero Flag
• AF : Auxiliary Carry Flag
• PF : Parity Flag
• CF : Carry Flag
Compte tenu de la ressemblance avec l'instruction SUB, les types des opérandes traités sont :
• CMP registre, mémoire
• CMP mémoire, registre
• CMP registre, registre
• CMP mémoire, immédiat
• CMP registre, immédiat
Instruction
Instruction Description Condition Condition
opposée
CF = 0 et ZF
JA Jump if Above JNA CF = 1 ou ZF = 1
=0
Jump if Above
JAE CF = 0 JNAE CF = 1
or equal
JB Jump if Below CF = 1 JNB CF = 0
Jump if Below CF = 1 ou
JBE JNBE CF = 0 et ZF = 0
or Equal ZF = 1
JC Jump if Carry CF = 1 JNC CF = 0
JE Jump if Equal ZF = 1 JNE ZF = 0
ZF = 0 et SF
JG Jump if Greater JNG ZF = 1 ou SF != OF
= OF
Jump if Greater
JGE SF = OF JNGE SF != OF
or Equal
JL Jump if Less SF != OF JNL SF = OF
Jump if Less or SF != OF ou
JLE JNLE SF = OF et ZF = 0
Equal ZF = 1
Jump if
JO OF = 1 JNO OF = 0
Overflow
JP Jump if Parity PF = 1 JNP PF = 0
Jump if Parity
JPE PF = 1 JNPE PF = 0
Even
Jump if Parity
JPO PF = 0 JNPO PF = 1
Odd
JS Jump if Sign SF = 0 JNS SF = 1
JZ Jump if Zero ZF = 1 JNZ ZF = 0
Table Instructions de branchement en assembleur
La première manière d'implanter des boucles en utilisant les mnémoniques de sauts conditionnels
(JNZ, JMP ...) : cela se rapproche du while et du do ... while.
La seconde méthode (qui se rapproche plutôt du for) consiste à utiliser le registre CX comme un
compteur par l’intermédiaire d’une des mnémoniques suivantes :
Instruc
Description
tion
Décrémente CX et va à l’étiquette
LOOP
si CX != 0
LOOP Décrémente CX et va à l’étiquette
E si CX != 0 et ZF = 1
LOOP Décrémente CX et va à l’étiquette
NE si CX != 0 et ZF = 0
LOOP Décrémente CX et va à l’étiquette
NZ si CX != 0 et ZF = 0
LOOP Décrémente CX et va à l’étiquette
Z si CX != 0 et ZF = 1
JCXZ va à l’étiquette si CX = 0
Table Instructions de boucle en assembleur
• nous devons d’abord écrire son nom (une étiquette mais sans les 2 points) suivi de la
directive PROC pour signaler à l’assembleur qu'il s'agit d'une procédure.
• nous plaçons ensuite les instructions assembleur correspondant à l'implantation de la
procédure (la dernière étant l’instruction RET).
• nous terminons le code de la procédure par une ligne comportant le nom de la procédure
suivi de la directive ENDP.
L'écriture d'une procédure peut se résumer par le squelette de code ci-dessous :
Name PROC
….
RET
Name ENDP
Lorsqu’une procédure est implantée, elle peut être appelée par l'instruction CALL suivie du nom de
la procédure. Le mnémonique sauvegarde l’IP dans la pile, indiquant ainsi l'adresse de retour. La
procédure est déroulée jusqu’à l’instruction RET qui restaure l’IP depuis la pile et qui entraîne
l’exécution de l’instruction qui suit CALL.
Lorsque nous quittons une procédure appelante pour entrer dans une procédure appelée, nous
devons sauvegarder le contexte d’exécution de la procédure appelante afin de pouvoir reprendre son
exécution lors du retour de la procédure appelée. Ce contexte d’exécution de la procédure appelante
est défini par le contenu de l’ensemble des registres qu’elle utilise :
L’instruction PUSHF permet de conserver le contenu du registre d’état à l’adresse SS:[SP]. Comme
ce registre a une taille de 16 bits SP est décrémentée de 2 (octets) à l’issue de cette opération.
L’instruction POPF effectue le traitement inverse de PUSHF. POPF permet de copier le contenu
stocké à l’adresse SS:[SP] vers le registre d’état. Comme ce registre a une taille de 16 bits SP est
incrémentée de 2 (octets) à l’issue de cette opération.
7. Les interruptions
Lors d’une utilisation normale d’un ordinateur, il y a en permanence l’exécution d’instructions.
Toutes ces instructions ne dépendent pas d’un même programme. En effet, il y a plusieurs
programmes qui se partagent le CPU, les entrées/sorties et donc pour exécuter les instructions, il y a
un procédé cyclique d’interruption / sauvegarde / exécution / restauration. Il existe un ensemble
d’interruptions :
Interruption matérielle
Si nous rentrons dans les détails, le contrôleur d’interruption est composé de 2 circuits 8259A mis
en cascade. Dont chaque fil, appelé IRQ (Interrupt ReQuest) est raccordé à un périphérique (ou un
ensemble de périphériques).
Le bouton "Code Examples" donne accès à une liste d'exemple. Le programme Hello World étant
trop complexe à ce stade de l’apprentissage (il faut d'abord décrire le fonctionnement de la carte
vidéo pour comprendre cet exemple), nous allons nous orienter vers un programme de calcul avec
des "ADD/ SUB". Pour utiliser les opérations, nous devons au préalable charger dans les registres
des valeurs, et nous le ferons avec l’instruction MOV et les registres de bases AX et BX.
• Empty workspace et
• valider.
Les instructions opération registre, valeur sont dites à adressage immédiat car nous indiquons "en
dur" la valeur qui doit être chargée dans le registre :
Les instructions opération registre, registre sont dites de registres car les données, sur lesquelles les
opérations sont effectuées, sont contenues dans les registres :
MOV BX, AX; CodeOp = 8BD8
Pour générer et commencer à exécuter le programme de la figure ci-dessus, il faut cliquer sur
Computer Systems – ICT1, tiré du cours de Pr. Cyril-Alexandre PACHON
21
EMULATE. L'assembleur attribue aux instructions traduite en langage machine des adresses
relatives. Le processus d’émulation respecte les conditions architecturales car les adresses sont
attribuées de façon séquentielle à partir du début du programme. C’est la raison pour laquelle une
origine doit être définie; fait par l’instruction ORG 100h.
Un programme, pour être exécuté est chargé en mémoire par le système d’exploitation. Le DOS
distingue 2 modèles de programmes :
1. les fichiers exécutables COM (utilisant un seul segment dans la mémoire de taille 64 Ko).
2. les fichiers exécutables EXE (limités que par la mémoire disponible dans l’ordinateur).
Le DOS charge le fichier COM et lui alloue toute la mémoire disponible. Mais si la mémoire est
insuffisante, il annule le chargement en l’indiquant à l’utilisateur.