5. Avantages et inconvénients de l'assembleur - UQAC
Un processeur réel a toutefois trop de registres et d'instructions pour pouvoir les
étudier en détail. .... nous restreindre au langage assembleur du
microprocesseur INTEL 8086. .... Le registre de segment définit une zone
mémoire de 64K, et le registre ... AF. a. 6. Zéro. ZF. z. 7. Signe. SF. s. 11.
débordement (overflow). OF. o ...
Part of the document
Une introduction à l'assembleur
1. Introduction
Le langage assembleur est très proche du langage machine (c'est-à-dire le
langage qu'utilise l'ordinateur: des informations en binaire, soit des 0 et
des 1). Il dépend donc fortement du type de processeur. Ainsi il n'existe
pas un langage assembleur, mais un langage assembleur par type de
processeur. Il est donc nécessaire de connaître un minimum le
fonctionnement d'un processeur pour pouvoir aborder cette partie. Un
processeur réel a toutefois trop de registres et d'instructions pour
pouvoir les étudier en détail. C'est pour cette raison que seuls les
registres et les instructions d'un processeur simple (Intel 80x86 16 bits)
seront étudiés. 2. Assembleur
L'assembleur est un langage de programmation (c'est-à-dire un moyen pour
l'homme de communiquer avec la machine) de très bas niveau (entendez par là
"très près de la machine"). En effet, la plupart des langages de
programmation (C/C++, Pascal, Java, etc...) ont pour but de simplifier la
tâche du programmeur en lui proposant des instructions "prêtes à l'emploi"
pour les tâches habituelles d'un programme. Par exemple, pour afficher
un texte à l'écran, en langage C, vous faites tout naturellement :
printf("Hello world!\n");
Mais en assembleur, il est nécessaire de comprendre comment ça ce passe au
niveau du processeur (sauf si vous utilisez les interruptions du DOS, bien
sûr, mais nous verrons cela plus tard...). De plus, en général, il faut
beaucoup de lignes de code pour faire pas grand chose... et le temps de
programmation en est d'autant plus long. Mais alors, quels sont les
avantages de l'assembleur ? En fait, étant donné que vous programmez
directement le processeur, vous pouvez vous-même effectuer des
optimisations sur votre code, suivant les cas ; ce que le compilateur ne
fait pas.
3. Différences entre le compilateur et l'assembleur
Tout microprocesseur contient en lui-même un jeu d'instructions. Ces
instructions, très basiques, se résument à une tâche simple, par exemple,
"mettre telle valeur dans la mémoire", ou "additionner telle valeur avec
telle autre valeur et mettre le résultat quelque part en mémoire". On est
loin du printf du C ! Autrement dit, l'assembleur va convertir un fichier
source contenant les instructions du microprocesseur sous forme de
mnémoniques anglaises en un fichier exécutable contenant le code numérique
binaire de chaque instruction, et donc compréhensible par le
microprocesseur. L'assembleur ne fait que traduire le fichier source du
langage humain vers le langage binaire. Par exemple, additionner 2 et 3
produit le code suivant en assembleur:
mov AX,2
add AX,3
Traduit en langage binaire, il donnera :
101110000000001000000000000001010000001100000000 ( B80200050300 en
hexadécimal )
On comprend mieux l'intérêt des mnémoniques et de l'assembleur !
Le compilateur, lui, analyse un fichier source écrit en un langage dit
"structuré", et transforme chaque instruction propre au langage en une
suite d'instructions machines, donc il convertit le fichier source en
programme assembleur, et ce n'est qu'ensuite qu'est produit le fichier
exécutable contenant les codes binaires. En fait, le compilateur effectue
une étape de plus que l'assembleur, c'est la transformation "fichier source
écrit en langage structuré" vers "fichier source écrit en assembleur".
C'est justement l'inconvénient du compilateur : la transformation n'est pas
toujours aussi bonne que ce qu'elle pourrait. Evidemment, si vous voulez
créer un programme qui affiche "Hello, world !", ou qui calcule la somme de
deux nombres, l'utilisation de l'assembleur est inutile, car même si le
compilateur produit un code assembleur moins bon que ce qu'on pourrait
faire directement en assembleur, la perte de vitesse lors de l'exécution du
programme ne sera pas perceptible... En revanche, si vous voulez créer une
application graphique ou un jeu, l'assembleur vous permettra d'obtenir des
fonctions graphiques rapides, à condition bien sûr de bien connaître toutes
les subtilités de l'assembleur, de manière à produire un code meilleur que
celui du compilateur.
4. Un peu plus de détails L'assembleur permet de contrôler directement la CPU. Cela permet d'avoir
une totale maîtrise du système et surtout permet de faire des programmes
rapides par rapport aux langages de haut niveau (C++, Basic, ...). En
effet, bien que ces langages permettent de faire des programmes facilement
et rapidement, ils n'optimisent pas le code d'exécution. Cela engendre donc
des programmes (beaucoup) plus volumineux. Notons que l'on peut insérer de
l'assembleur dans certain langage (Pascal et C par exemple) pour accélérer
l'exécution du programme. Par exemple, void main(void) { int A = 20;
asm{
MOV AX, A
SHL AX, 1
}
printf(''AX =%d\n'',_AX);
}
Ce programme affiche 40.
En fait, la meilleure chose à faire pour pouvoir utiliser à la fois
l'assembleur et le langage C (ou tout autre langage évolué), c'est de créer
des fonctions en asm, de les assembler en tant que fichier objet, d'écrire
des fichiers en-têtes (extension .h) pour déclarer vos fonctions au
compilateur, puis de linker vos fichiers objets à votre exécutable final.
Mais avant d'aller plus loin, rappelons brièvement la fonction de l'unité
centrale (CPU). Comme nous l'avons vu dans le précédent chapitre, la CPU (Central
Processing Unit) charge, analyse et exécute les instructions présentes en
mémoire de façon séquentielle, c'est-à-dire une instruction à la suite de
l'autre. Elle contient une unité de calculs arithmétiques et logiques
(UAL), d'un bus interne, d'un bus externe se connectant au système, d'un
décodeur d'instructions qui décode l'instruction en cours, et des registres
pour mémoriser des résultats temporairement.
Ce sont les registres qui permettent la communication entre le programme et
la CPU. Ils sont 'l'interface' de la CPU. En effet, pratiquement, toutes
les données qui passent par la CPU, pour être traitées par celle-ci,
doivent se trouver dans les registres de cette dernière. Ces cases mémoire
sont les plus rapides de tout le système.
Il existe différents types de registres dans une CPU: les registres de
traitements, les registres d'adressages et les registres d'état. Les
Registres de traitement sont des registres destinés au traitement des
valeurs contenues dans celle-ci; par exemple on peut effectuer une addition
d'un registre de traitement avec un autre registre, effectuer des
multiplications ou des traitements logiques. Les Registres d'adressage
permettent de pointer un endroit de la mémoire; ils sont utilisés pour lire
ou écrire dans la mémoire. Les registres d'état (ou volet : FLAG en
anglais) sont de petits registres (de 1 Bit) indiquant l'état du processeur
et 'le résultat' de la dernière instruction exécutée. Les plus courants
sont le Zero Flag (ZF) qui indique que le résultat de la dernière opération
est égale a zéro (après une soustraction par exemple), le Carry Flag (CF)
qui indique qu'il y a une retenue sur la dernière opération effectuée,
Overflow Flag (OF) qui indique un dépassement de capacité de registre, etc.
Pour gérer ces registres, on fait appel aux instructions du processeur. Ces
instructions permettent d'effectuer des tâches très simples sur les
registres. Elles permettent de mettre des valeurs dans les registres,
d'effectuer des traitements logiques, des traitements arithmétiques, des
traitements de chaîne de caractères, etc.
Ces instructions sont formées à l'aide du code binaire dans le programme.
Or pour nous, il est plus facile d'utiliser des symboles à la place de ces
codes binaires. C'est pour cela que l'on fait appel à l'assembleur qui
permet de transformer un programme écrit en langage assembleur fait avec
des mots clés compréhensibles pour nous (mais incompréhensible pour la
machine), en un programme exécutable compréhensible par le processeur.
Ces mots clés, ou mnémoniques, sont souvent la compression d'un mot ou
d'une expression en anglais présentant l'action de l'instruction. Par
exemple sur les processeurs 8086, l'instruction MUL permet la
multiplication (MULtiply), sur Z80 l'instruction LD permet de charger une
valeur dans un registre ou dans la mémoire (Load)
Les instructions sont souvent suivies d'opérandes permettant d'indiquer sur
quel(s) registre(s) on veut effectuer le traitement, quelle valeur on veut
utiliser, etc. Exemple: Pour additionner 2 registres (2 registres 16 bits AX et BX) sur
8086: ADD AX,BX
Cette instruction correspond en gros a: AX=AX+BX La même chose sur 68000: ADD.W D1,D Cette instruction correspond en gros a
D0=D0+D1 Sur cet exemple, nous pouvons remarquer la différence de syntaxe entre deux
processeurs différents: lorsqu'un constructeur conçoit un processeur, il
détermine aussi son assembleur. C'est pour cela que l'assembleur n'est pas
universel car il est différent sur chaque processeur. Et même si par hasard
une instruction est identique entre deux CPU différentes, elle ne sera pas
compatible sur chaque processeur car le code binaire correspondant sera
différent. Ceci est l'un des plus gros inconvénients de la programmation en
assembleur car il faut alors reprogrammer de fond en comble le logiciel si
on veut le porter sur un autre processeur. Chaque processeur comporte un
nombre plus ou moins important d'instructions.
Dans ce c