Cours : Les bases de l'assembleur
Le but de ce cours n'est pas de vous apprendre à coder en assembleur, cela prendrait bien plus qu'une journée. L'idée est qu'à la fin de cette journée, vous ayez une idée de ce qui se passe au niveau de votre ordinateur lorsqu'il doit exécuter les instructions contenues dans votre programme.
Fonctionnement d'un ordinateur
Il y a de multiples composants dans votre ordinateur, mais les 3 principaux qui nous intéressent aujourd'hui sont le processeur, la mémoire vive et le système d'exploitation.
Le processeur (CPU)
Dans un ordinateur le processeur ou CPU (Central Processing Unit) c'est le cerveau. C'est le composant qui se charge d'effectuer toutes les opérations mathématiques et de manipulation de la mémoire.
On estime la puissance d'un processeur au nombre d'opérations qu'il peut effectuer en 1 seconde, exprimé en hertz. Un processeur à 1 Ghz (giga hertz) peut donc effectuer 10^9 (1 000 000 000) opérations par seconde.
La mémoire vive (RAM)
Dans un ordinateur la mémoire vive ou RAM (Random Access Memory) c'est la mémoire à court terme.
Pour vous votre mémoire à court terme, c'est celle par exemple qui vous permet de stocker temporairement un numéro de téléphone qu'on vous a dicté avant de le noter.
Sa durée de stockage est limitée. En prime, dans un ordinateur, on y accède de façon aléatoire, c'est-à-dire qu'il n'est pas nécessaire de la parcourir en entier pour accéder à une "case" précise.
Le système d'exploitation (OS)
Le système d'exploitation, c'est un logiciel très bas niveau, qui parle très directement à la machine et permet d'automatiser les échanges de l'utilisateur avec elle. Il manipule le système de gestion des fichiers, gère la priorité des programmes exécutés, la gestion des signaux entrants (souris, clavier, ...) et sortants (écran, enceintes, ...).
L'assembleur
Le principe
Votre ordinateur ne comprend que deux informations : il y a du courant (1) ou il n'y a pas de courant (0). Pour lui cette information de ce qui doit ou non avoir du courant est représentée par une suite de 1 et de 0 : le binaire.
Votre processeur est ce qui va interpréter les 0 et les 1 pour les transformer en opérations. Dans votre ordinateur tout est représenté par des nombres stockés dans 8 cases contenant des 1 et des 0 : les octets. Un octet contient très exactement un nombre qui peut parfois représenter un symbole (la table ASCII vous donne une sorte de pierre de rosette pour transformer certains nombres en symboles).
Il serait très complexe et fastidieux pour un humain de devoir rédiger l'intégralité des opérations que l'on veut effectuer en binaire. On a donc inventé une forme de langage simplifié pour parler au processeur, que l'on appelle le langage assembleur.
1 processeur = 1 assembleur
Il existe une multitude de processeurs, avec des architectures et technologies très différentes, y compris dans des environnements techniques très contrôlés.
Pour cette raison chaque modèle de processeur utilise un langage assembleur différent. Il y a certes des similitudes, mais les différences sont telles qu'un programme en assembleur qui marche par exemple sur un processeur M1 d'Apple ne fonctionnera pas directement sur un M2.
Exemple de programme complexe en assembleur
Il est assez rare de voir les programmes en assembleur de nos jours, mais il existe sur GitHub des repository publics avec des programmes absolument mythiques en assembleur.
C'est le cas par exemple du module d'alunissage du programme Apollo 11 qui avait amené Neil Armstrong et Buzz Aldrin sur la Lune. La NASA a fait passer ce code dans le domaine public et il peut être trouvé à cette adresse : https://github.com/chrislgarry/Apollo-11
Code soumis par Margaret H. Hamilton la cheffe de programmation Colossus et du programme de guidage et de navigation Apollo.
L'extrait suivant préparait la liste d'attente des tâches à effectuer (cette partie du programme permettait de créer les fonctionnalités de mise en attente d'une tâche dans le module de commande) :
Le principe de la compilation
Lorsque vous écrivez du code en C et que vous le compilez, ce que fait le compilateur c'est d'abord de traduire votre code en C en une liste d'instructions en assembleur. Mais il ne le transforme pas en un code assembleur lisible par l'humain, mais dans un code compréhensible par la machine généralement appelé bytecode.
Le bytecode ressemble à quelque chose comme ceci :
La plupart du temps des suites de nombre en hexadécimal (un nombre en base 16) mais pas toujours.
Le code et sa compilation
Vous, en tant que dev, allez écrire du code en langage assembleur. Lorsque votre processeur va recevoir ce code, la première chose que va faire le petit programme interne du processeur c'est transformer votre série d'instructions en bytecode.
L'exécution du programme
Une fois le programme transformé en bytecode, le processeur va utiliser ces informations pour exécuter chacune des instructions une par une.
Pour exécuter les instructions, le processeur va utiliser un certain nombre de registres (des boîtes permettant de contenir des nombres et qui ont chacun un rôle précis) et des accès à la mémoire vive. Il va donc stocker des nombres directement dans ces circuits (les registres) et des informations pour plus ou moins longtemps (la RAM est réinitialisée à chaque démarrage de l'ordinateur) dans la mémoire vive.
Notre assembleur de démonstration
Parce que chaque processeur utilise un assembleur différent et parfois très complexe, nous allons utiliser une sorte d'assembleur simplifié, qui reprend les grands principes communs mais en enlevant une partie de la complexité.
Pour les curieuses et les curieux, cet assembleur de démonstration est composé d'une API basique en PHP et d'une mini-application front en JavaScript Vanilla. Le code est disponible ici : https://github.com/kornog-dev/asm-demo.
Les registres
Notre assembleur utilise 4 registres différents :
storage
Le registre storage stocke des valeurs lues dans la mémoire ou à écrire dans la mémoire.
condition
Le registre condition sert à stocker des valeurs qui vont être utilisées pour déterminer si une condition est vraie ou non.
cursor
Le registre cursor sert à stocker la position actuelle du curseur dans la mémoire vive.
program
Le registre program stocke le numéro de l'instruction actuelle du programme.
Les instructions
Notre langage assembleur utilise 8 instructions différentes :
add
add [register] [value]
Ajouter value au registre register
sub
sub [register] [value]
Soustraire value au registre register
get
get [register]
Stocke la valeur de register dans le registre storage
set
set [register]
Stocke la valeur de storage dans le registre register
read
read
Stocke la valeur de la case du curseur actuel de la mémoire dans le registre storage
write
write
Stocke la valeur du registre storage dans la case du curseur actuel de la mémoire
jump
jump [instruction_number]
Change la valeur du registre program en instruction_number
zjump
zjump [value] [instruction_number]
Si le registre condition contient value change la valeur du registre program en instruction_number
Notre interface de démonstration
https://asm-demo.kornog-formations.com/app/

Exemples d'opération simple
Créer une variable
Pour créer une variable, nous allons devoir stocker une valeur à une position précise dans la mémoire. Nous allons donc donner une valeur au registre cursor (add cursor 0) puis placer la valeur que nous voulons écrire dans le registre storage (add storage 8) et enfin nous allons dire au programme d'écrire cette valeur à l'emplacement de cursor (write).
Créer une condition
Pour créer une condition, nous devons stocker une valeur dans le registre condition (add condition 5). Une condition seule en assembleur ne sert à rien, elle est utilisée pour savoir si on doit ou non sauter vers une instruction.
Créer une boucle
Pour créer une boucle en assembleur, il faut créer un programme qui revient à sa première instruction jusqu'à ce qu'une condition soit vraie. Ici, nous allons revenir à notre seconde instruction (jump 1) tant que la valeur de condition n'est pas égale à 3. Quand elle est égale à 3 nous terminons le programme (zjump 3 4). Pour éviter de faire une boucle infinie, nous allons augmenter la valeur de condition à chaque fois (add condition 1).
exit n'est pas une vraie instruction elle nous permet simplement d'avoir un numéro d'instruction sur lequel nous rendre.
Exercices guidés avec l'assembleur de démonstration
Pour créer nos programmes, nous allons les écrire au format JSON et les stocker dans des fichiers .json que nous pouvons ensuite importer dans l'interface de démonstration.
Voici le format du fichier pour la création d'une variable :
Écrire le résultat d'une addition dans la mémoire
Le but de cet exercice est d'effectuer une addition puis d'en inscrire le résultat dans la mémoire.
Vous allez donc devoir écrire le nombre 6 à l'emplacement numéro 4 de la mémoire. Vous obtenir ce nombre 6 vous allez devoir faire deux additions successives.
Créer une boucle qui fait 5 tours
Le but de cet exercice est de créer une boucle qui fera 5 tours avant de se terminer.
Numéroter la mémoire
Le but de cet exercice est d'écrire dans chaque case de la mémoire son numéro d'emplacement.