0% ont trouvé ce document utile (0 vote)
40 vues30 pages

Introduction à l'assembleur 80x86

ARCHITECTURE DES ORDINATEURS

Transféré par

eliana.daniel
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats DOCX, PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
40 vues30 pages

Introduction à l'assembleur 80x86

ARCHITECTURE DES ORDINATEURS

Transféré par

eliana.daniel
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats DOCX, PDF, TXT ou lisez en ligne sur Scribd

CHAPITRE V.

L’assembleur 80x86

V.1 L’assembleur
V.1.1 Pourquoi l'assembleur ?

Lorsque l'on doit lire ou écrire un programme en


langage machine, il est difficile d'utiliser la notation
hexadécimale.

On écrit les programmes à l'aide de symboles comme


MOV, ADD, etc.

Les concepteurs du processeur fournissent toujours


une documentation avec les codes des instructions de
leur processeur, et les symboles correspondant.

le programmeur doit spécifier lui même les adresses


des données et des instructions.

Soit par exemple le programme suivant, qui multiplie


une donnée en mémoire par 8 :
0100 MOV BX, [0112] ; charge la donnée
0103 MOV AX, 3
0106 SHL BX ; décale à gauche
0108 DEC AX
0109 JNE 0106 ; recommence 3 fois
010B MOV [0111], BX ; range le résultat
010E MOV AH, 4C
0110 INT 21H
0112 ; on range ici la donnée

Nous avons spécifié que la donnée était rangée à


l'adresse 0111H, et que l'instruction de branchement
JNE allait en 0106H.

Si on désire modifier légèrement ce programme, par


exemple ajouter une instruction avant MOV [0111]
BX, il va falloir modifier ces deux adresses.

On conçoit aisément que ce travail devient très difficile


si le programme manipule beaucoup de variables.

L'utilisation d'un assembleur résout ces problèmes.

L'assembleur permet en particulier de nommer les


variables et de repérer par des étiquettes certaines
instructions sur lesquelles on va effectuer des
branchements.
V.1.2 De l'écriture du programme à son exécution
L'assembleur est un utilitaire qui n'est pas interactif.
Le programme qu'on désire traduire en langage
machine (on dit assembler) doit être placé dans un
fichier texte (avec l'extension .ASM sous DOS).

La saisie du programme source au clavier nécessite un


programme appelé éditeur de texte.
L'opération d'assemblage traduit chaque instruction du
programme source en une instruction machine.
Le résultat de l'assemblage est enregistré dans un
fichier avec l'extension .OBJ (fichier objet ).

Le fichier .OBJ n'est pas directement exécutable :


En effet, il arrive fréquemment qu'on construise un
programme exécutable à partir de plusieurs fichiers
sources.
Il faut ``relier'' les fichiers objets à l'aide d'un utilitaire
nommé éditeur de lien (même si l'on en a qu'un seul).
L'éditeur de liens fabrique un fichier exécutable , avec
l'extension .EXE.

Le fichier .EXE est directement exécutable.


Un utilitaire spécial du système d'exploitation, le
chargeur est responsable de la lecture du fichier
exécutable, de son implantation en mémoire
principale, puis du lancement du programme.
V.1.3 Structure du programme source
La structure générale d'un programme assembleur est
représentée comme suit.
Data SEGMENT ; data est le nom du segment de
données
Directives de déclaration de données
Data ENDS ; fin du segment de données
Assume DS:data, CS:code
Code SEGMENT ; code est le nom du segment
d’instructions
Debut : ; 1ere instruction, avec l’étiquette debut
; Suite d’instructions code ENDS
END debut ; fin du programme, avec l'étiquette ; de la
première instruction.

Figure V.1: Structure d'un programme en assembleur

Comme tout programme, un programme écrit en


assembleur comprend des définitions de données et
des instructions, qui s'écrivent chacune sur une ligne
de texte.
Les données sont déclarées par des directives, mots
clef spéciaux que comprend l'assembleur.
Les directives qui déclarent des données sont
regroupées dans le segment de données, qui est
délimité par les directives SEGMENT et ENDS.

Les instructions sont placées dans un autre segment,


le segment de code.
La directive ASSUME est toujours présente et sera
expliquée plus loin (section ).
La première instruction du programme (dans le
segment d'instruction) doit toujours être repérée par
une étiquette.
Le fichier doit se terminer par la directive END avec le
nom de l'étiquette de la première instruction.
ceci permet d'indiquer à l'éditeur de liens quelle est
la première instruction à exécuter lorsque l'on lance
le programme.
Les points-virgules indiquent des commentaires.
V.1.4 Déclaration de variables
On déclare les variables à l'aide de directives.
L'assembleur attribue à chaque variable une adresse.
Dans le programme, on repère les variables grâce à leur
nom.
Les noms des variables (comme les étiquettes ) sont
composés d'une suite de 31 caractères au maximum,
commençant obligatoirement par une lettre.
Le nom peut comporter des majuscules, des
minuscules, des chiffres, plus les caractères @, ? et _.

Lors de la déclaration d'une variable, on peut lui


affecter une valeur initiale.

V.1.4.1 Variables de 8 ou 16 bits

Les directives DB (Define Byte) et DW (Define Word)


permettent de déclarer des variables de
respectivement 1 ou 2 octets.

Exemple d'utilisation :
data SEGMENT
entrée DW 15 ; 2 octets initialisés à 15
sortie DW ? ; 2 octets non initialisés
clé DB ? ; 1 octet non initialisé
nega DB -1 ; 1 octet initialisé à -1 data
ENDS
Les valeurs initiales peuvent être données en
hexadécimal (constante terminée par H) ou en binaire
(terminée par b) :
data SEGMENT truc DW 0F0AH
; en hexa masque DB 01110000b ;
en binaire data ENDS

Les variables s'utilisent dans le programme en les


désignant par leur nom.
Après la déclaration précédente, on peut écrire par
exemple :
MOV AX, truc
AND AL, masque
MOV truc, AX
L'assembleur se charge de remplacer les noms de
variable par les adresses correspondantes.
V.1.4.2 Tableaux
Il est aussi possible de déclarer des tableaux, c'est à
dire des suites d'octets ou de mots consécutifs. Pour
cela, utiliser plusieurs valeurs initiales :
data SEGMENT
machin DB 10, 0FH ; 2 fois 1 octet
chose DB -2, 'ALORS'
data ENDS
Remarquez la déclaration de la variable chose : un
octet à -2 (=FEH), suivi d'une suite de caractères.
L'assembleur n'impose aucune convention pour la
représentation des chaînes de caractères : c'est à
l'utilisateur d'ajouter si nécessaire un octet nul pour
marquer la fin de la chaîne.

Après chargement de ce programme, la mémoire aura


le contenu suivant :

FEH chose
41H chose + 1
4CH chose + 2
4FH chose + 3
52H chose + 4
53H chose + 5

Début du segment data 0AH machin

OFH machin + 1
Si on veut écrire un caractère X à la place du O de
ALORS, on pourra écrire :

MOV AL, 'X'


MOV chose+1, AL
Notons que chose+1 est une constante (valeur
connue au moment de l'assemblage) : l'instruction
générée par
l'assembleur pour
MOV chose +1, AL est
MOV [adr], AL .
VI.1.4.3 Directive dup

Lorsque l'on veut déclarer un tableau de n cases, toutes


initialisées à la même valeur, on utilise la directive
dup :

tab DB 100 dup (15) ; 100 octets valant 15 zzz DW


10 dup (?) ; 10 mots de 16 bits non
initialisés

V.2 Segmentation de la mémoire


Nous abordons ici le problème de la segmentation de la
mémoire.
Les données sont normalement regroupées dans une
zone mémoire nommée segment de données.
Les instructions étaient placées dans un segment
d'instructions.
Ce partage se fonde sur la notion plus générale de
segment de mémoire, qui est à la base du mécanisme
de gestion des adresses par les processeurs 80x86.
Nous avons vu plus haut que les instructions utilisaient
normalement des adresses codées sur 16 bits.
Nous savons aussi que le registre IP, qui stocke
l'adresse d'une instruction, fait lui aussi 16 bits.
Or, avec 16 bits il n'est possible d'adresser que 2 16 = 64
Kilo octets.
Le bus d'adresse du 80486 possède 32 bits.
Cette adresse de 32 bits est formée par la juxtaposition
d'un registre segment (16 bits de poids fort) et d'un
déplacement (offset , 16 bits de poids faible).
Les adresses que nous avons manipulé jusqu'ici sont
des déplacements.
Le schéma suivant illustre la formation d'une adresse
32 bits à partir du segment et du déplacement sur 16
bits :
On appellera segment de mémoire une zone mémoire
adressable avec une valeur fixée du segment (les 16
bits de poids fort).
Un segment a donc une taille maximale de 64 Ko.

V.2 .1 Segment de code et de données

La valeur du segment est stockée dans des registres


spéciaux de 16 bits.
Le registre DS (Data Segment) est utilisé pour le
segment de données, et le registre CS (Code Segment)
pour le segment d'instructions.
V.2 .1 .1Registre CS
Lorsque le processeur lit le code d'une instruction,
l'adresse 32 bits est formée à l'aide du registre segment
CS et du registre déplacement IP.

La paire de ces deux registres est notée CS:IP.

V.2 .1.2 Registre DS

Le registre DS est utilisé pour accéder aux données


manipulées par le programme. Ainsi, l'instruction

MOV AX, [0145]

donnera lieu à la lecture du mot mémoire d'adresse


DS:0145H.
V.2 .2 Initialisation des registres segment
Dans ce cours, nous n'écrirons pas de programmes
utilisant plus de 64 Ko de code et 64 Ko de données.
ceci nous permettra de n'utiliser qu'un seul segment de
chaque type.
Par conséquent, la valeur des registres CS et de DS sera
fixée une fois pour toute au début du programme.
Le programmeur en assembleur doit se charger de
l'initialisation de DS, c'est à dire de lui affecter l'adresse
du segment de données à utiliser.
Par contre, le registre CS sera automatiquement
initialisé sur le segment contenant la première
instruction au moment du chargement en mémoire du
programme (par le chargeur du système
d'exploitation).

V.2 .3 Déclaration d'un segment en assembleur

Comme nous l'avons vu, les directives SEGMENT et


ENDS permettent de définir les segments de code et de
données.
La directive ASSUME permet d'indiquer à l'assembleur
quel est le segment de données et celui de code, afin
qu'il génère des adresses correctes.
Enfin, le programme doit commencer, avant toute
référence au segment de données, par initialiser le
registre segment DS, de la façon suivante :
MOV AX, nom_segment_de_données
MOV DS, AX
(Il serait plus simple de faire MOV DS,
nom_segment_de_données mais il se trouve que
cette instruction n'existe pas.)
La figure VI.2 donne un exemple complet de
programme assembleur.
Programme calculant la somme de deux entiers de 16
bits
data SEGMENT
A DW 10 ; A = 10
B DW 1789 ; B = 1789 Result
DW ? ; resultat data ENDS code
SEGMENT
ASSUME DS:data, CS:code
Debut : MOV AX, data ; étiquette car 1ere instruction
MOV DS, AX ; initialise DS ; Le
programme:
MOV AX, A
ADD AX, B
MOV result, AX ; range le résultat

; Retour au DOS:
MOV AH, 4CH INT 21H
code ENDS
END Debut ; étiquette de la 1ere instruction.

Figure V.2 Exemple de programme en assembleur

V. 3 Adressage indirect

Nous introduisons ici un nouveau mode d'adressage,


l'adressage indirect : très utile par exemple pour traiter
des tableaux.

L'adressage indirect utilise le registre BX pour stocker


l'adresse d'une donnée.
En adressage direct, on note l'adresse de la donnée
entre crochets :
MOV AX, [130] ; adressage direct
De façon similaire, on notera en adressage indirect :
MOV AX, [BX] ; adressage indirect

Ici, BX contient l'adressage de la donnée.


L'avantage de cette technique est que l'on peut
modifier l'adresse en BX, par exemple pour accéder à la
case suivante d'un tableau.

Avant d'utiliser un adressage indirect, il faut charger BX


avec l' adresse d'une donnée.
Pour cela, on utilise une nouvelle directive de
l'assembleur, offset.
data SEGMENT truc
DW 1996 data
ENDS
...
MOV BX, offset truc
...

Si l'on avait employé la forme

MOV BX, truc

on aurait chargé dans BX la valeur stockée en truc (ici


1996), et non son adresse.
V.3.1 Exemple : parcours d'un tableau

Voici un exemple plus complet utilisant l'adressage


indirect.

Ce programme passe une chaîne de caractères en


majuscules.

La fin de la chaîne est repérée par un caractère $.

On utilise un ET logique pour masquer le bit 5 du


caractère et le passer en majuscule (voir le code ASCII).
data SEGMENT
tab DB 'Uu bbbbb Bbbbbbbbbbb', '$'
data ENDS

code SEGMENT
ASSUME DS:data, CS:code

debut: MOV AX, data


MOV DS, AX

MOV BX, offset tab ; adresse debut tableau

repet: MOV AL, [BX] ; lis 1 caractère


AND AL, 11011111b ; force bit 5 a zero
MOV [BX], AL ; range le caractère
INC BX ; passe au suivant CMP
AL, '$' ; arrive au $ final ?
JNE repet ; sinon recommencer

MOV AH, 4CH


INT 21H ; Retour au DOS
code ENDS
END debut

V.3.2 Spécification de la taille des données


Dans certains cas, l'adressage indirect est ambigu.
Par exemple, si l'on écrit
MOV [BX], 0 ; range 0 à l'adresse spécifiée par BX
l'assembleur ne sait pas si l'instruction concerne 1, 2 ou
4 octets consécutifs.
Afin de lever l'ambiguïté, on doit utiliser une directive
spécifiant la taille de la donnée à transférer :
MOV byte ptr [BX], val ; concerne 1 octet
MOV word ptr [BX], val ; concerne 1 mot de 2 octets

V.4 La pile
V.4.1 Notion de pile

Les piles offrent un nouveau moyen d'accéder à des


données en mémoire principale, qui est très utilisé
pour stocker temporairement des valeurs.
Une pile est une zone de mémoire et un pointeur qui
conserve l'adresse du sommet de la pile.
V.4.2 Instructions PUSH et POP
Deux nouvelles instructions, PUSH et POP , permettent
de manipuler la pile.
PUSH registre empile le contenu du
registre sur la pile.

POP registre
retire la valeur en haut de la pile et la place dans
le registre spécifié.
Exemple : transfert de AX vers BX en passant par la pile.
PUSH AX ; Pile <- AX
POP BX ; BX <- Pile

(Note : cet exemple n'est pas très utile, il vaut mieux


employer MOV BX, AX.)

La pile est souvent utilisée pour sauvegarder


temporairement le contenu des registres : AX et BX
contiennent des données à conserver

PUSH AX
PUSH BX

MOV BX, truc ; on utilise AX


ADD AX, BX ; et BX
MOV truc, AX
POP BX ; récupère l'ancien BX POP
AX ; et l'ancien AX

On voit que la pile peut conserver plusieurs valeurs.

La valeur dépilée par POP est la dernière valeur


empilée;

c'est pourquoi on parle ici de pile LIFO (Last In First Out,


Premier Entré Dernier Sorti).
V.4.3 Registres SS et SP
La pile est stockée dans un segment séparé de la
mémoire principale.
Le processeur possède deux registres dédiés à la
gestion de la pile, SS et SP.
Le registre SS (Stack Segment) est un registre segment
qui contient l'adresse du segment de pile courant (16
bits de poids fort de l'adresse).
Il est normalement initialisé au début du programme et
reste fixé par la suite.

Le registre SP (Stack Pointer) contient le déplacement du sommet de la


pile (16 bits de poids faible de l’ adresse).
Figure V.3: La pile.

SP pointe sur le sommet (dernier emplacement


occupé).
La figure V.3 donne une représentation schématique de
la pile.
L'instruction PUSH effectue les opérations suivantes :

• SP  SP - 2
• [SP]  valeur du registre 16 bits.
Notons qu'au début (pile vide), SP pointe ``sous'' la pile.
L'instruction POP effectue le travail inverse :

• registre destination  [SP]


• SP  SP + 2
Si la pile est vide, POP va lire une valeur en dehors de
l'espace pile, donc imprévisible.
V.4.4 Déclaration d'une pile

Pour utiliser une pile en assembleur, il faut déclarer un


segment de pile, et y réserver un espace suffisant.

Ensuite, il est nécessaire d'initialiser les registres SS et


SP pour pointer sous le sommet de la pile. Voici la
déclaration d'une pile de 200 octets :
seg_pile SEGMENT stack ; mot clef stack car pile
DW 100 dup (?) ; reserve espace base_pile EQU
this word ; etiquette base de la pile seg_pile ENDS

Noter le mot clef ``stack '' après la directive SEGMENT,


qui indique à l'assembleur qu'il s'agit d'un segment de
pile.

Afin d'initialiser SP, il faut repérer l'adresse du bas de la


pile; c'est le rôle de la ligne base_pile EQU this word

Figure V.4: Une pile vide.


L'étiquette base-pile repère la base de la pile, valeur
initiale de SP.
Après les déclarations ci-dessus, on utilisera la
séquence d'initialisation :

ASSUME SS:seg_pile

MOV AX, seg_pile


MOV SS, AX ; init Stack Segment

MOV SP, base_pile ; pile vide

Noter que le registre SS s'initialise de façon similaire au


registre DS; par contre, on peut accéder directement
au registre SP.

V.5 Procédures

V.5.1 Notion de procédure


La notion de procédure en assembleur correspond à
celle de fonction en langage C, ou de sous-programme
dans d'autres langages.
Figure VI.5: Appel d'une procédure.

La procédure est nommée calcul.

Après l'instruction B, le processeur passe à l'instruction


C de la procédure, puis continue jusqu'à rencontrer RET
et revient à l'instruction D.
Une procédure est une suite d'instructions effectuant
une action précise.
Ces instructions sont regroupées par commodité et
pour éviter d'avoir à les écrire à plusieurs reprises dans
le programme.
Les procédures sont repérées par l'adresse de leur
première instruction, à laquelle on associe une
étiquette en assembleur.
L'exécution d'une procédure est déclenchée par un
programme appelant.
Une procédure peut elle-même appeler une autre
procédure, et ainsi de suite.
V.5.2 Instructions CALL et RET

L'appel d'une procédure est effectué par l'instruction


CALL.

CALL adresse_debut_procedure

L'adresse est sur 16 bits, la procédure est donc dans le


même segment d'instructions.

CALL est une nouvelle instruction de branchement


inconditionnel.
La fin d'une procédure est marquée par l'instruction
RET :
RET ne prend pas d'argument; le processeur passe à
l'instruction placée immédiatement après le CALL.
RET est aussi une instruction de branchement : le
registre IP est modifié pour revenir à la valeur qu'il
avait avant l'appel par CALL.
Comment le processeur retrouve-t-il cette valeur ?
Le problème est compliqué par le fait que l'on peut
avoir un nombre quelconque d'appels imbriqués,
comme sur la figure.
L'adresse de retour, utilisée par RET, est en fait
sauvegardée sur la pile par l'instruction CALL.
Lorsque le processeur exécute l'instruction RET, il dépile l'adresse sur la pile (comme POP), et
la range dans IP.

Figure V.6: Plusieurs appels de procédures imbriqués.

L'instruction CALL effectue donc les opérations :


• Empiler la valeur de IP. A ce moment, IP pointe sur
l'instruction qui suit le CALL.

• Placer dans IP l'adresse de la première instruction


de la procédure (donnée en argument).
Et l'instruction RET :
• Dépiler une valeur et la ranger dans IP.

V.5.3 Déclaration d'une procédure

L'assembleur possède quelques directives facilitant la


déclaration de procédures.
On déclare une procédure dans le segment
d'instruction comme suit :
Calcul PROC near ; procédure nommée Calcul
... ; instructions

RET ; dernière instruction


Calcul ENDP ; fin de la procédure

Le mot clef PROC commence la définition d'une


procédure.

near indiquant qu'il s'agit d'une procédure située dans


le même segment d'instructions que le programme
appelant.
L'appel s'écrit simplement :
CALL Calcul
V.5.4 Passage de paramètres

En général, une procédure effectue un traitement sur


des données (paramètres ).

Ces paramètres sont fournies par le programme


appelant.

Une procédure produit un résultat qui est transmis à ce


programme.
Plusieurs stratégies peuvent être employées :
[Link] par registre : les valeurs des paramètres
sont contenues dans des registres du processeur.
C'est une méthode simple, mais qui ne convient
que si le nombre de paramètres est petit (il y a peu
de registres).
[Link] par la pile : les valeurs des paramètres
sont empilées. La procédure lit la pile.

V.5.4 .1 Exemple avec passage par registre

On va écrire une procédure ``SOMME'' qui calcule la


somme de 2 nombres naturels de 16 bits.
Convenons que les entiers sont passés par les registres
AX et BX, et que le résultat sera placé dans le registre
AX.
La procédure s'écrit alors très simplement :
SOMME PROC near ; AX <- AX + BX
ADD AX, BX
RET SOMME
ENDP

et son appel, par exemple pour ajouter 6 à la variable


Truc :

MOV AX, 6
MOV BX, Truc
CALL SOMME
MOV Truc, AX
V.5.4.2 Exemple avec passage par la pile

Cette technique met en oeuvre un nouveau registre, BP


(Base Pointer), qui permet de lire des valeurs sur la pile
sans les dépiler ni modifier SP.
Le registre BP permet un mode d'adressage indirect
spécial, de la forme : MOV AX, [BP+6]

cette instruction charge le contenu du mot mémoire


d'adresse BP+6 dans AX.
Ainsi, on lira le sommet de la pile avec :
MOV BP, SP ; BP pointe sur le sommet
MOV AX, [BP] ; lit sans dépiler

et le mot suivant avec :


MOV AX, [BP+2] ; 2 car 2 octets par mot de pile.

L'appel de la procédure ``SOMME2'' avec passage par la


pile est :
PUSH 6
PUSH Truc
CALL SOMME2
La procédure SOMME2 va lire la pile pour obtenir la
valeur des paramètres.
Pour cela, il faut bien comprendre quel est le contenu
de la pile après le CALL :

SP IP ( adresse de retour)

SP+2 Truc ( premier paramètre)


SP+4 6 ( deuxième paramètre)

Le sommet de la pile contient l'adresse de retour


(ancienne valeur de IP empilée par CALL).

Chaque élément de la pile occupe deux octets.


La procédure SOMME2 s'écrit donc :
SOMME2 PROC near ; AX <- arg1 + arg2
MOV BP, SP ; adresse sommet pile
MOV AX, [BP+2] ; charge argument 1
ADD AX, [BP+4] ; ajoute argument 2
RET
SOMME2 ENDP
La valeur de retour est laissée dans AX.
La solution avec passage par la pile parait plus lourde
sur cet exemple simple.
Cependant, elle est beaucoup plus souple dans le cas
général que le passage par registre.
Il est très facile par exemple d'ajouter deux paramètres
supplémentaires sur la pile.
Une procédure bien écrite modifie le moins de registres
possible.
En général, l'accumulateur est utilisé pour transmettre
le résultat et est donc modifié.
Les autres registres utilisés par la procédure seront
normalement sauvegardés sur la pile.
Voici une autre version de SOMME2 qui ne modifie pas
la valeur contenue par BP avant l'appel :
SOMME2 PROC near ; AX <- arg1 + arg2
PUSH BP ; sauvegarde BP
MOV BP, SP ; adresse sommet pile
MOV AX, [BP+4] ; charge argument 1
ADD AX, [BP+6] ; ajoute argument 2
POP BP ; restaure ancien BP
RET
SOMME2 ENDP

Noter que les index des arguments (BP+4 et


BP+6) sont modifiés car on a ajouté une valeur au
sommet de la pile.

Vous aimerez peut-être aussi