Assembleur
Un langage d'assemblage est un langage de programmation qui peut être utilisé pour dire directement à l'ordinateur ce qu'il doit faire. Un langage d'assemblage est presque exactement comme le code machine qu'un ordinateur peut comprendre, sauf qu'il utilise des mots à la place des chiffres. Un ordinateur ne peut pas vraiment comprendre directement un programme en assembleur. Cependant, il peut facilement transformer le programme en code machine en remplaçant les mots du programme par les chiffres qu'ils représentent. Un programme qui fait cela est appelé un assembleur.
Les programmes écrits en langage assembleur sont généralement constitués d'instructions, qui sont de petites tâches que l'ordinateur effectue lorsqu'il exécute le programme. On les appelle des instructions parce que le programmeur les utilise pour indiquer à l'ordinateur ce qu'il doit faire. La partie de l'ordinateur qui suit les instructions est le processeur.
Le langage d'assemblage d'un ordinateur est un langage de bas niveau, ce qui signifie qu'il ne peut être utilisé que pour effectuer les tâches simples qu'un ordinateur peut comprendre directement. Pour effectuer des tâches plus complexes, il faut dire à l'ordinateur chacune des tâches simples qui font partie de la tâche complexe. Par exemple, un ordinateur ne comprend pas comment imprimer une phrase sur son écran. Au lieu de cela, un programme écrit en assembleur doit lui dire comment faire toutes les petites étapes qui sont impliquées dans l'impression de la phrase.
Un tel programme d'assemblage serait composé de nombreuses instructions qui, ensemble, font quelque chose qui semble très simple et fondamental pour un humain. Il est donc difficile pour les humains de lire un programme d'assemblage. En revanche, un langage de programmation de haut niveau peut avoir une seule instruction, comme IMPRIMER "Bonjour, monde !", qui dira à l'ordinateur d'effectuer toutes les petites tâches à votre place.
Développement du langage de l'assemblée
Lorsque les informaticiens ont commencé à construire des machines programmables, ils les ont programmées directement en code machine, c'est-à-dire une série de chiffres qui indiquaient à l'ordinateur ce qu'il devait faire. L'écriture du langage machine était très difficile et prenait beaucoup de temps, si bien qu'on a fini par créer un langage d'assemblage. Le langage assembleur est plus facile à lire pour un humain et peut être écrit plus rapidement, mais il est toujours beaucoup plus difficile à utiliser pour un humain qu'un langage de programmation de haut niveau qui essaie d'imiter le langage humain.
Programmation en code machine
Pour programmer en code machine, le programmeur doit savoir à quoi ressemble chaque instruction en binaire (ou en hexadécimal). Bien qu'il soit facile pour un ordinateur de comprendre rapidement ce que signifie le code machine, c'est difficile pour un programmeur. Chaque instruction peut avoir plusieurs formes, qui ressemblent toutes à un tas de chiffres pour les gens. Toute erreur commise lors de l'écriture d'un code machine ne sera remarquée que si l'ordinateur fait la mauvaise chose. Il est difficile de déterminer l'erreur car la plupart des gens ne peuvent pas dire ce que signifie un code machine en le regardant. Voici un exemple de code machine :
05 2A 00
Ce code machine hexadécimal indique à un processeur informatique x86 d'ajouter 42 à l'accumulateur. Il est très difficile pour une personne de le lire et de le comprendre, même si elle connaît le code machine.
Utiliser le langage de l'assemblée à la place
Avec le langage d'assemblage, chaque instruction peut être écrite sous la forme d'un mot court, appelé mnémonique, suivi d'autres choses comme des chiffres ou d'autres mots courts. La mnémonique est utilisée pour que le programmeur n'ait pas à se souvenir des chiffres exacts en code machine nécessaires pour dire à l'ordinateur de faire quelque chose. Parmi les exemples de mnémoniques en langage assembleur, on peut citer add, qui ajoute des données, et mov, qui déplace des données d'un endroit à un autre. Comme le mot "mnémonique" est peu courant, on utilise parfois à la place l'expression "type d'instruction" ou "instruction", souvent de manière incorrecte. Les mots et les chiffres qui suivent le premier mot donnent plus d'informations sur ce qu'il faut faire. Par exemple, les choses qui suivent un ajout peuvent indiquer ce que deux choses doivent ajouter ensemble et les choses qui suivent un déménagement indiquent ce qu'il faut déplacer et où le mettre.
Par exemple, le code machine de la section précédente (05 2A 00) peut être écrit en assembleur comme :
Le langage assembleur permet également aux programmeurs d'écrire plus facilement les données réelles utilisées par le programme. La plupart des langages d'assemblage permettent de créer facilement des chiffres et du texte. En code machine, chaque type de nombre différent, comme le positif, le négatif ou le décimal, doit être converti manuellement en binaire et le texte doit être défini une lettre à la fois, comme des nombres.
Le langage assembleur fournit ce que l'on appelle une abstraction du code machine. Lorsqu'il utilise l'assemblage, le programmeur n'a pas besoin de connaître les détails de la signification des nombres pour l'ordinateur, c'est plutôt l'assembleur qui s'en charge. En fait, le langage assembleur permet toujours au programmeur d'utiliser toutes les fonctionnalités du processeur qu'il pourrait utiliser avec du code machine. En ce sens, le langage assembleur a une très bonne et rare caractéristique : il a la même capacité à exprimer les choses que ce qu'il est en train d'abstraire (code machine) tout en étant beaucoup plus facile à utiliser. De ce fait, le code machine n'est presque jamais utilisé comme langage de programmation.
Démontage et débogage
Lorsque les programmes sont terminés, ils ont déjà été transformés en code machine afin que le processeur puisse les exécuter. Parfois, cependant, si le programme comporte un bug (erreur), les programmeurs voudront pouvoir dire ce que fait chaque partie du code machine. Les désassembleurs sont des programmes qui aident les programmeurs à faire cela en transformant le code machine du programme en langage assembleur, qui est beaucoup plus facile à comprendre. Les désassembleurs, qui transforment le code machine en langage assembleur, font l'inverse des assembleurs, qui transforment le langage assembleur en code machine.
Organisation informatique
Il faut comprendre comment les ordinateurs sont organisés, comment ils semblent fonctionner à un niveau très bas, pour comprendre comment fonctionne un programme en langage assembleur. Au niveau le plus simpliste, les ordinateurs ont trois parties principales :
- la mémoire principale ou RAM qui contient les données et les instructions,
- un sous-traitant, qui traite les données en exécutant les instructions, et
- entrée et sortie (parfois raccourcies en I/O), qui permettent à l'ordinateur de communiquer avec le monde extérieur et de stocker les données en dehors de la mémoire principale afin de pouvoir les récupérer plus tard.
Mémoire principale
Dans la plupart des ordinateurs, la mémoire est divisée en octets. Chaque octet contient 8 bits. Chaque octet en mémoire possède également une adresse, qui est un numéro indiquant l'endroit où se trouve l'octet en mémoire. Le premier octet en mémoire a une adresse de 0, le suivant a une adresse de 1, et ainsi de suite. Le fait de diviser la mémoire en octets permet de l'adresser car chaque octet a une adresse unique. Les adresses des mémoires d'octets ne peuvent pas être utilisées pour se référer à un seul bit d'un octet. Un octet est le plus petit morceau de mémoire qui peut être adressé.
Même si une adresse fait référence à un octet particulier en mémoire, les processeurs permettent d'utiliser plusieurs octets de mémoire à la suite. L'utilisation la plus courante de cette fonctionnalité consiste à utiliser 2 ou 4 octets de suite pour représenter un nombre, généralement un entier. Les octets simples sont parfois aussi utilisés pour représenter des nombres entiers, mais comme ils ne font que 8 bits, ils ne peuvent contenir que 28 ou 256 valeurs différentes possibles. L'utilisation de 2 ou 4 octets à la suite porte le nombre de valeurs possibles à 216, 65536 ou 232, 4294967296, respectivement.
Lorsqu'un programme utilise un octet ou un nombre d'octets à la suite pour représenter quelque chose comme une lettre, un chiffre ou autre chose, ces octets sont appelés un objet parce qu'ils font tous partie de la même chose. Même si les objets sont tous stockés dans des octets de mémoire identiques, ils sont traités comme s'ils avaient un "type", qui indique comment les octets doivent être compris : soit comme un nombre entier, soit comme un caractère ou un autre type (comme une valeur non entière). Le code machine peut également être considéré comme un type qui est interprété comme une instruction. La notion de type est très, très importante car elle définit ce qui peut et ne peut pas être fait à l'objet et comment interpréter les octets de l'objet. Par exemple, il n'est pas valable de stocker un nombre négatif dans un objet de nombre positif et il n'est pas valable de stocker une fraction dans un entier.
Une adresse qui pointe vers (est l'adresse d') un objet à plusieurs octets est l'adresse du premier octet de cet objet - l'octet qui a l'adresse la plus basse. Par ailleurs, il est important de noter que l'adresse ne permet pas de déterminer le type d'un objet, ni même sa taille. En fait, vous ne pouvez même pas dire de quel type est un objet en le regardant. Un programme en langage assembleur doit savoir quelles adresses mémoire contiennent quels objets, et quelle est la taille de ces objets. Un programme qui fait cela est sans danger pour le type d'objet car il ne fait que des choses aux objets qui sont sans danger pour leur type. Un programme qui ne le fait pas ne fonctionnera probablement pas correctement. Notez que la plupart des programmes ne stockent pas explicitement le type d'un objet, ils accèdent simplement aux objets de manière cohérente - le même objet est toujours traité comme étant du même type.
Le processeur
Le processeur exécute des instructions, qui sont stockées sous forme de code machine dans la mémoire principale. En plus de pouvoir accéder à la mémoire pour le stockage, la plupart des processeurs disposent de quelques petits espaces rapides et de taille fixe pour contenir les objets avec lesquels on travaille actuellement. Ces espaces sont appelés registres. Les processeurs exécutent généralement trois types d'instructions, bien que certaines instructions puissent être une combinaison de ces types. Vous trouverez ci-dessous quelques exemples de chaque type en langage assembleur x86.
Instructions qui permettent de lire ou d'écrire la mémoire
L'instruction suivante en langage assembleur x86 lit (charge) un objet de 2 octets à partir de l'octet à l'adresse 4096 (0x1000 en hexadécimal) dans un registre de 16 bits appelé "ax" :
Dans cette langue d'assemblage, les crochets entourant un numéro (ou un nom de registre) signifient que le numéro doit être utilisé comme une adresse pour les données qui doivent être utilisées. L'utilisation d'une adresse pour pointer vers des données s'appelle l'indirection. Dans l'exemple suivant, sans les crochets, un autre registre, bx, reçoit en fait la valeur 20.
Comme aucune méthode indirecte n'a été utilisée, la valeur réelle elle-même a été inscrite dans le registre.
Si les opérandes (les choses qui viennent après la mnémonique), apparaissent dans l'ordre inverse, une instruction qui charge quelque chose à partir de la mémoire l'écrit au lieu de l'écrire en mémoire :
Ici, la mémoire à l'adresse 1000h obtient la valeur de ax. Si cet exemple est exécuté juste après le précédent, les 2 octets à 1000h et 1001h seront un entier de 2 octets avec la valeur de 20.
Instructions qui effectuent des opérations mathématiques ou logiques
Certaines instructions font des choses comme la soustraction ou des opérations logiques comme pas :
L'exemple de code machine présenté plus haut dans cet article serait celui-ci en langage assembleur :
Ici, 42 et ax sont additionnés et le résultat est stocké dans ax. Dans l'assemblage x86, il est également possible de combiner un accès mémoire et une opération mathématique de cette manière :
Cette instruction ajoute la valeur de l'entier de 2 octets stocké à 1000h à ax et stocke la réponse dans ax.
Cette instruction calcule le ou du contenu des registres ax et bx et stocke le résultat dans ax.
Les instructions qui décident de ce que sera la prochaine instruction
Habituellement, les instructions sont exécutées dans l'ordre où elles apparaissent en mémoire, c'est-à-dire dans l'ordre où elles sont tapées dans le code d'assemblage. Le processeur les exécute simplement l'une après l'autre. Cependant, pour que les processeurs puissent faire des choses compliquées, ils doivent exécuter des instructions différentes en fonction des données qui leur ont été données. La capacité des processeurs à exécuter des instructions différentes en fonction du résultat de quelque chose s'appelle le branchement. Les instructions qui décident de ce que doit être la prochaine instruction sont appelées instructions de branchement.
Dans cet exemple, supposons que quelqu'un veuille calculer la quantité de peinture dont il aura besoin pour peindre un carré d'une certaine longueur de côté. Toutefois, en raison des économies d'échelle, le magasin de peinture ne lui vendra pas moins que la quantité de peinture nécessaire pour peindre un carré de 100 x 100.
Pour déterminer la quantité de peinture qu'ils devront obtenir en fonction de la longueur du carré qu'ils veulent peindre, ils proposent cette série d'étapes :
- soustraire 100 de la longueur du côté
- si la réponse est inférieure à zéro, fixez la longueur du côté à 100
- multiplier la longueur du côté par elle-même
Cet algorithme peut être exprimé dans le code suivant où ax est la longueur du côté.
Cet exemple introduit plusieurs nouveautés, mais les deux premières instructions sont familières. Elles copient la valeur de ax dans bx et soustraient ensuite 100 de bx.
L'une des nouveautés de cet exemple s'appelle un label, un concept que l'on retrouve dans les langues d'assemblage en général. Les étiquettes peuvent être tout ce que le programmeur veut (sauf s'il s'agit du nom d'une instruction, ce qui confondrait l'assembleur). Dans cet exemple, le label est "continue". Il est interprété par l'assembleur comme l'adresse d'une instruction. Dans ce cas, il s'agit de l'adresse de mult ax.
Un autre nouveau concept est celui des drapeaux. Sur les processeurs x86, de nombreuses instructions fixent des "drapeaux" dans le processeur qui peuvent être utilisés par l'instruction suivante pour décider de ce qu'il faut faire. Dans ce cas, si bx était inférieur à 100, sub mettra un drapeau qui indique que le résultat était inférieur à zéro.
L'instruction suivante est jge, qui est l'abréviation de "Jump if Greater or Equal to". Il s'agit d'une instruction de branche. Si les drapeaux dans le processeur spécifient que le résultat était supérieur ou égal à zéro, au lieu de simplement passer à l'instruction suivante, le processeur sautera à l'instruction au niveau de l'étiquette continue, qui est mul ax.
Cet exemple fonctionne bien, mais ce n'est pas ce que la plupart des programmeurs écriraient. L'instruction subtract a correctement positionné le drapeau, mais elle modifie également la valeur sur laquelle elle opère, ce qui a nécessité la copie de ax dans bx. La plupart des langages d'assemblage permettent des instructions de comparaison qui ne modifient aucun des arguments qui leur sont passés, mais qui placent quand même les drapeaux correctement et l'assemblage x86 ne fait pas exception.
Maintenant, au lieu de soustraire 100 de ax, de voir si ce nombre est inférieur à zéro, et de l'assigner à nouveau à ax, ax reste inchangé. Les drapeaux sont toujours placés de la même façon, et le saut est toujours effectué dans les mêmes situations.
Entrées et sorties
Bien que les entrées et les sorties soient une partie fondamentale de l'informatique, il n'y a pas une seule façon de les faire en langage assembleur. En effet, le fonctionnement des entrées/sorties dépend de la configuration de l'ordinateur et du système d'exploitation qu'il utilise, et pas seulement du type de processeur dont il dispose. Dans la partie "exemple", l'exemple "Hello World" utilise les appels du système d'exploitation MS-DOS et l'exemple suivant utilise les appels du BIOS.
Il est possible de faire des entrées/sorties en langage assembleur. En effet, le langage assembleur peut généralement exprimer tout ce qu'un ordinateur est capable de faire. Cependant, même s'il y a des instructions pour ajouter et brancher en langage assembleur qui feront toujours la même chose, il n'y a pas d'instructions en langage assembleur qui font toujours des E/S.
Il est important de noter que le fonctionnement des E/S ne fait partie d'aucun langage assembleur car il ne fait pas partie du fonctionnement du processeur.
Langues d'assemblage et portabilité
Même si le langage assembleur n'est pas directement exécuté par le processeur - le code machine l'est, il y a tout de même beaucoup à faire. Chaque famille de processeurs supporte des caractéristiques, des instructions, des règles différentes pour ce que les instructions peuvent faire, et des règles pour la combinaison d'instructions qui sont autorisées à tel ou tel endroit. C'est pourquoi les différents types de processeurs ont toujours besoin de langages d'assemblage différents.
Comme chaque version du langage assembleur est liée à une famille de processeurs, il lui manque quelque chose que l'on appelle la portabilité. Une chose qui a de la portabilité ou qui est portable peut être facilement transférée d'un type d'ordinateur à un autre. Alors que d'autres types de langages de programmation sont portables, le langage assembleur, en général, ne l'est pas.
Langue de l'assemblée et langues de haut niveau
Bien que le langage assembleur permette d'utiliser facilement toutes les fonctionnalités du processeur, il n'est pas utilisé pour les projets de logiciels modernes pour plusieurs raisons :
- Il faut beaucoup d'efforts pour exprimer un programme simple en assemblée.
- Bien qu'il ne soit pas aussi sujet aux erreurs que le code machine, le langage d'assemblage offre encore très peu de protection contre les erreurs. Presque tous les langages d'assemblage n'imposent pas la sécurité des caractères.
- Le langage d'assemblage ne favorise pas les bonnes pratiques de programmation comme la modularité.
- Si chaque instruction individuelle en langage assembleur est facile à comprendre, il est difficile de dire quelle était l'intention du programmeur qui l'a écrite. En fait, le langage assembleur d'un programme est si difficile à comprendre que les entreprises ne s'inquiètent pas des personnes qui désassemblent (obtiennent le langage assembleur de) leurs programmes.
En raison de ces inconvénients, des langages de haut niveau comme Pascal, C et C++ sont utilisés à la place pour la plupart des projets. Ils permettent aux programmeurs d'exprimer leurs idées plus directement au lieu de devoir se soucier de dire au processeur ce qu'il doit faire à chaque étape. On les appelle de haut niveau parce que les idées que le programmeur peut exprimer dans la même quantité de code sont plus compliquées.
Les programmeurs qui écrivent du code dans des langages compilés de haut niveau utilisent un programme appelé compilateur pour transformer leur code en langage assembleur. Les compilateurs sont beaucoup plus difficiles à écrire que les assembleurs. De plus, les langages de haut niveau ne permettent pas toujours aux programmeurs d'utiliser toutes les fonctionnalités du processeur. C'est parce que les langages de haut niveau sont conçus pour supporter toutes les familles de processeurs. Contrairement aux langages assembleurs, qui ne supportent qu'un seul type de processeur, les langages de haut niveau sont portables.
Même si les compilateurs sont plus compliqués que les assembleurs, des décennies de fabrication et de recherche les ont rendus très performants. Maintenant, il n'y a plus beaucoup de raisons d'utiliser le langage assembleur pour la plupart des projets, parce que les compilateurs peuvent généralement trouver comment exprimer les programmes en langage assembleur aussi bien ou mieux que les programmeurs.
Exemples de programmes
Un programme Hello World écrit en assemblée x86 :
Une fonction qui imprime un numéro à l'écran en utilisant les interruptions du BIOS écrites en assemblage NASM x86. Il est possible d'écrire un code modulaire en assembleur, mais cela demande un effort supplémentaire. Notez que tout ce qui vient après un point-virgule sur une ligne est un commentaire et est ignoré par l'assembleur. Il est très important de mettre des commentaires dans le code en langage assembleur car les grands programmes en langage assembleur sont très difficiles à comprendre.
Questions et réponses
Q : Qu'est-ce qu'un langage d'assemblage ?
R : Un langage d'assemblage est un langage de programmation qui peut être utilisé pour dire directement à l'ordinateur ce qu'il doit faire. Il est presque exactement comme le code machine qu'un ordinateur peut comprendre, sauf qu'il utilise des mots à la place des chiffres.
Q : Comment un ordinateur peut-il comprendre un programme assembleur ?
R : Un ordinateur ne peut pas vraiment comprendre un programme d'assemblage directement, mais il peut facilement transformer le programme en code machine en remplaçant les mots du programme par les chiffres qu'ils représentent. Ce processus est réalisé à l'aide d'un assembleur.
Q : Que sont les instructions dans un langage d'assemblage ?
R : Les instructions d'un langage d'assemblage sont de petites tâches que l'ordinateur effectue lorsqu'il exécute le programme. Elles sont appelées instructions car elles indiquent à l'ordinateur ce qu'il doit faire. La partie de l'ordinateur chargée de suivre ces instructions s'appelle le processeur.
Q : Quel type de langage de programmation est le langage d'assemblage ?
R : Le langage d'assemblage est un langage de programmation de bas niveau, ce qui signifie qu'il ne peut être utilisé que pour effectuer des tâches simples qu'un ordinateur peut comprendre directement. Pour effectuer des tâches plus complexes, il faut décomposer chaque tâche en ses composants individuels et fournir des instructions pour chaque composant séparément.
Q : En quoi cela diffère-t-il des langages de haut niveau ?
R : Les langages de haut niveau peuvent avoir des commandes uniques telles que PRINT "Hello, world !" qui indiqueront à l'ordinateur d'effectuer toutes ces petites tâches automatiquement sans avoir besoin de les spécifier individuellement comme vous devriez le faire avec un programme d'assemblage. Cela rend les langages de haut niveau plus faciles à lire et à comprendre pour les humains que les programmes d'assemblage composés de nombreuses instructions individuelles.
Q : Pourquoi est-il difficile pour les humains de lire un programme d'assemblage ?
R : Parce que de nombreuses instructions individuelles doivent être spécifiées afin de réaliser une tâche complexe telle que l'impression de quelque chose à l'écran ou l'exécution de calculs sur des ensembles de données - des choses qui semblent très basiques et simples lorsqu'elles sont exprimées en langage humain naturel - il peut donc y avoir de nombreuses lignes de code constituant une seule instruction, ce qui rend difficile pour les humains qui ne connaissent pas le fonctionnement interne des ordinateurs à un niveau aussi bas de suivre et d'interpréter ce qui s'y passe.