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" :
mov ax, [1000h]
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.
mov bx, 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 :
mov [1000h], ax
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 :
ajouter ax, 42
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 :
ajouter ax, [1000h]
Cette instruction ajoute la valeur de l'entier de 2 octets stocké à 1000h à ax et stocke la réponse dans ax.
ou ax, bx
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é.
mov bx, ax sous-bx, 100 jge continuer mov ax, 100 continue : mul ax
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.
cmp ax, 100 jge continuer mov ax, 100 continue : mul ax
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.