Fonctionnement d'un ordinateur/Langage machine et assembleur

Un livre de Wikilivres.
Aller à : navigation, rechercher

Ce chapitre va aborder le langage machine d'un processeur, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boite noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. La majorité des concepts qui seront vus dans ce chapitre ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des instructions du processeur. Pour simplifier, on peut classer les instructions en trois grands types :

  • les échanges de données entre mémoires ;
  • les calculs et autres opérations arithmétiques ;
  • les instructions de comparaison ;
  • les instructions de branchement.

À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc.

Instructions d'accès mémoire[modifier | modifier le wikicode]

Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la mémoire. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les instructions : LOAD, STORE et MOV.

  • LOAD est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM.
  • STORE fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM.
  • MOV copie le contenu d'un registre dans un autre, sans passer par la mémoire (le contenu du registre de destination est perdu).

Instructions de calcul[modifier | modifier le wikicode]

Tout ordinateur gère des instructions qui font des calculs arithmétiques simples. Ces instructions dépendent de la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres signés, des nombres codés en complément à 1, des flottants simple précision, des flottants double précision, etc. Pour gérer ces différents types de données, le processeur dispose souvent d'une instruction par type à manipuler : on peut avoir une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. Sur d'autres machines assez anciennes, on stockait le type de la donnée (est-ce un flottant, un entier codé en BCD, etc.) dans la mémoire. Chaque nombre manipulé par le processeur incorporait un tag, une petite suite de bits qui permettait de préciser son type. Le processeur ne possédait pas d'instructions en plusieurs exemplaires pour faire la même chose, et utilisait le tag pour déduire comment faire ses calculs. Par exemple, ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Le traitement effectué par cette instruction dépendait du tag incorporé dans la donnée. Des processeurs de ce type s'appellent des architectures à tags, ou tagged architectures.

Les tous premiers ordinateurs pouvaient manipuler des données de taille arbitraire. Alors certes, ces processeurs n'utilisaient pas vraiment les encodages de nombres qu'on a vus au premier chapitre. À la place, ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. De nos jours, les ordinateurs utilisent des entiers de taille fixe. La taille des données à manipuler peut dépendre de l'instruction. Ainsi, un processeur peut avoir des instructions pour traiter des nombres entiers de 8 bits, et d'autres instructions pour traiter des nombres entiers de 32 bits, par exemple. On peut aussi citer le cas des flottants : il faut bien faire la différence entre flottants simple précision et double précision !

Instructions arithmétiques[modifier | modifier le wikicode]

Les instructions arithmétiques sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, éventuellement la division, parfois les opérations plus complexes comme la racine carrée.

La division est une opération très complexe et particulièrement lente, bien plus qu'une addition ou une multiplication. Pour information, sur les processeurs actuels, la division est entre 20 à 80 fois plus lente qu'une addition/soustraction, et presque 7 à 26 fois plus lente qu'une multiplication. Mais on a de la chance : c'est aussi une opération assez rare. Un programme effectue rarement des divisions, les plus rares étant les divisions entières tandis que les divisions les plus fréquentes sont les divisons entre deux nombres flottants. Souvent, les divisions les plus couramment utilisées dans un programme sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effecteur des divisons par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Diverses astuces permettent de remplacer ces opérations de divisions par des suites d'instructions plus simples mais donnant le même résultat. J'ai parlé auparavant des décalages, qui permettent de remplacer de divisons par . Mais il existe d'autres méthodes, qui fonctionnent pur un grand nombre de constantes. Par exemple, on peut remplacer une division par une constante par une multiplication un peu bizarre : la multiplication par un entier réciproque). Sachant cela, certains processeurs ne possèdent pas d'instruction de division. Inclure une instruction de division n'accélérerait qu'un faible nombre d'instructions, et ne donnerait pas lieu à des gains assez importants en terme de performance : accélérer 1% des instructions d'un programme (ici, les divisions) en implémentant un circuit complexe et gourmand en transistors alors qu'on pourrait utiliser ces circuits pour câbler des instructions plus utiles serait du gâchis. Certains processeurs implémentent toutefois la division dans une instruction machine, disposant souvent d'un circuit dédié. Les gains ne sont pas forcément faramineux, mais ne sont pas forcément négligeables non plus.

Instructions flottantes[modifier | modifier le wikicode]

Les processeurs peuvent aussi gérer les calculs sur des nombres flottants. IEEE 754 standardise aussi quelques instructions sur les flottants qui doivent impérativement être supportées : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Le support de ces instructions est souvent matériel : le processeur possède souvent des instructions machine capables d'effectuer des comparaisons, additions, soustractions, multiplications, divisions et racines carrées. De nos jours, ces instructions sont directement câblées dans les circuits du processeur et ne sont plus émulées, sauf pour quelques-unes. Le choix du mode d'arrondi ou la gestion des exceptions sont implémentés directement dans le matériel et sont souvent configurables grâce à un registre du processeur : suivant la valeur mise dans celui-ci, le processeur arrondira les résultats des calculs d'une certaine façon et pas d'une autre, ou réagira d'une certaine manière aux exceptions vues au-dessus.

Il faut néanmoins préciser que le support de la norme IEEE 754 par la FPU/le jeu d'instruction n'est pas une obligation : certains processeurs s'en moquent royalement. Dans certaines applications, les programmeurs ont souvent besoin d'avoir des calculs qui s'effectuent rapidement et se contentent très bien d'un résultat approché. Dans ces situations, on peut utiliser des formats de flottants différents de la norme IEEE 754 et les circuits de la FPU sont simplifiés pour être plus rapides. Par exemple, certains circuits ne gèrent pas les underflow, overflow, les NaN ou les infinis, voir utilisent des formats de flottants exotiques. A coté, certaines architectures ont besoin que le compilateur ou les programmes fassent quelques manipulations pour que les calculs effectués avec des flottants respectent la norme IEEE 754. Tout dépend des instructions machines que le compilateur utilise. Par exemple, certains processeurs implémentent non seulement les instructions de la norme, mais aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres, sont supportées par certaines FPU. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul. Or, certaines de ces instructions sont parfois utilisées par certains compilateurs, ce qui peut produire des erreurs de calcul incompatibles avec la norme IEEE 754.

Instructions logiques[modifier | modifier le wikicode]

A coté des instructions de calcul, on trouve des instructions logiques qui travaillent sur des bits ou des groupes de bits. Nous allons détailler ces instructions avant de passer à la suite. La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. Vient ensuite le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. Il existe des instructions similaires avec le OU ou le XOR.

Les instructions de décalage décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages, dont deux vont nous intéresser particulièrement : les décalages logiques, et les décalages arithmétiques. Le décalage logique, ou logical shift, décale tout ses chiffres d'un ou plusieurs crans vers la gauche ou la droite, et remplit les vides par des zéros. Grâce à ce remplissage par des zéros, un décalage de n rangs vers la droite/gauche correspond à une multiplication ou division entière par 2^n, pour les entiers non signés. Mais lorsqu'on effectue un décalage à droite, certains bits vont sortir du résultat et être perdus : le résultat est tronqué ou arrondi vers zéro. Pour pouvoir effectuer des divisons par 2^n sur des nombres négatifs avec un décalage, on a inventé les décalages arithmétiques ou arithmetical shift. Ils sont similaires aux décalages logiques, si ce n'est que les vides laissés par le décalage sont remplis avec le bit de signe, et non pas des zéros. Ces instructions sont équivalentes à une multiplication/division par 2^n, que le nombre soit signé ou non. La différence avec un décalage logique est que les nombres négatifs sont arrondis vers moins l'infini. Pour donner un exemple, 92 sera arrondi en 4, tandis que −92 sera arrondi en −5. Les instructions de rotation sont similaires aux décalages, à part que les bits qui sortent du nombre d'un coté rentrent de l'autre et servent à boucher les trous.

Instructions de test[modifier | modifier le wikicode]

Les instructions de test comparent deux valeurs (des adresses, ou des nombres entiers ou à virgule flottante). Sur la majorité des processeurs, ces instructions ne font qu'une comparaison à la fois. D'autres processeurs ont des instructions de test qui effectuent plusieurs comparaisons en même temps et fournissent plusieurs résultats. Par exemple, un processeur x86 posséde une instruction CMP qui vérifie simultanément si deux valeurs A et B sont égales, différentes, si A est inférieur à B, si A est supérieur à B, etc. Les instructions de comparaison permettent souvent d'effectuer les comparaisons suivantes :

  • A > B (est-ce que A est supérieur à B ?) ;
  • A < B (est-ce que A est inférieur à B ?) ;
  • A == B (est-ce que A est égal à B ?) ;
  • A != B (est-ce que A est différent de B ?).

Il arrive que certaines de ces instructions de test effectuent plusieurs comparaisons en même temps et fournissent plusieurs résultats. Par exemple, un processeur peut très bien posséder une instruction cmp capable de vérifier si deux valeurs A et B sont égales, différentes, si A est inférieure à B, et si A est supérieur à B, en même temps.

Stockage du résultat d'une comparaison[modifier | modifier le wikicode]

Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Et pour ce faire, il existe plusieurs techniques :

  • soit on utilise des registres d'état ;
  • soit un utilise des registres à prédicats ;
  • soit on ne mémorise pas le résultat.

Certains processeurs incorporent un registre d'état, qui stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur. Le ou les bits du registre d'état modifiés par une instruction de test dépendent de l'instruction utilisée : par exemple, on peut utiliser un bit qui indiquera si l'entier testé est égal à un autre, un autre bit qui indiquera si le premier entier testé est supérieur à l'autre, etc. Ce registre peut aussi contenir d'autres bits suivant le processeur, comme le bit de débordement qui prévient quand le résultat d'une instruction est trop grand pour tenir dans un registre, le bit null qui précise que le résultat d'une instruction est nul (vaut zéro), le bit de retenue qui est utile pour les additions, le bit de signe qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif. Le registre d'état a un avantage : certaines instructions arithmétiques peuvent modifier les bits du registre d'état, ce qui leur permet parfois de remplacer une instruction de test. Par exemple, prenons le cas d'un processeur où la soustraction modifie le bit null du registre d'état : on peut tester si deux nombres sont égaux en soustrayant leur contenu, le bit null étant mis à zéro si c'est le cas.

D'autres processeurs utilisent des registres à prédicats, des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Cette méthode est plus souple que l'utilisation d'un registre d'état dont les bits ont une utilité fixée une fois pour toutes. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les utiliser par la suite. Par exemple, une instruction de branchement pourra vérifier la valeur de plusieurs de ces registres pour prendre une décision. Chose impossible avec les autres approches, dont les branchements ne peuvent utiliser qu'un seul résultat de comparaison pour prendre leur décision.

Enfin, sur d'autres processeurs, il n'y a pas de registres pour stocker les résultats de comparaisons : les comparaisons sont fusionnées avec les branchements en une seule instruction, rendant le stockage du résultat inutile.

Instructions de branchements[modifier | modifier le wikicode]

Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses structures de contrôle de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certaines langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce tutoriel : ce tutoriel ne va pas vous apprendre à programmer.

Nom de la structure de contrôle Description
SI...ALORS Exécute une suite d'instructions si une condition est respectée
SI...ALORS...SINON Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas.
Boucle WHILE...DO Répète une suite d'instructions tant qu'une condition est respectée.
Boucle DO...WHILE aussi appelée REPEAT UNTIL Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions.
Boucle FOR Répète un nombre fixé de fois une suite d'instructions.

Pour implémenter ces structures de contrôle, les instructions de test sont combinées avec des instructions de branchement. Ces branchements modifient la valeur stockée dans le registre d'adresse d'instruction, ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Il existe deux types de branchements :

  • les branchements inconditionnels, avec lesquels le processeur passe toujours à l'instruction vers laquelle le branchement va renvoyer ;
  • les branchements conditionnels, où le branchement n'est exécuté que si certains bits du registre d'état sont à une certaine valeur.

Sur la plupart des processeurs, les branchements conditionnels sont précédés d'une instruction de test ou de comparaison. Mais d'autres effectuent le test et le branchement en une seule instruction machine, pour se passer de registre d'état ou de registres à prédicats. Sur d'autres processeurs, les instructions de test permettent de zapper l'instruction suivante si la condition testée est fausse : cela permet de simuler un branchement conditionnel à partir d'un branchement inconditionnel. Sur quelques rares processeurs, le program counter est un registre général qui peut être modifié par n'importe quel opération arithmétique : cela permet de remplacer les branchements par une simple écriture dans le program counter.

Structures de contrôle conditionnelles[modifier | modifier le wikicode]

Généralement, les branchements servent à implémenter des fonctionnalités de langages programmation impératifs, appelées structures de contrôles. Celles-ci sont au nombre de plusieurs. Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse).

Structures de contrôle itératives[modifier | modifier le wikicode]

Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas.

Instructions d'appel et de retour de fonction[modifier | modifier le wikicode]

Certains branchements sont utilisés pour fabriquer des fonctions. Pour comprendre ce que sont ces fonctions, il faut faire quelques rappels. Un programme contient souvent des suites d'instructions présentes en plusieurs exemplaires, qui servent souvent à effectuer une tâche bien précise : calculer un résultat bien précis, communiquer avec un périphérique, écrire un fichier sur le disque dur, ou autre chose encore. Sans utiliser de sous-programmes, ces suites d'instructions sont présentes en plusieurs exemplaires dans le programme. Le programmeur doit donc recopier à chaque fois ces suites d'instructions, ce qui ne lui facilite pas la tache (sauf en utilisant l’ancêtre des sous-programmes : les macros). Et dans certains programmes, devoir recopier plusieurs fois la séquence d'instruction qui permet d'agir sur un périphérique ou de faire une action spéciale est franchement barbant ! De plus, ces suites d'instructions sont présentes plusieurs fois dans le programme final, exécuté par l'ordinateur. Et elles prennent de la place inutilement ! Dans les langages de programmation modernes, il est possible de ne conserver qu'un seul exemplaire en mémoire et l'utiliser au besoin. L'exemplaire en question est ce qu'on appelle une fonction, ou encore un sous-programme. C'est au programmeur de « sélectionner » ces suites d'instructions et d'en faire des fonctions.

Pour exécuter une fonction, il suffit d'exécuter un branchement dont l'adresse de destination est celle de la fonction : on dit qu'on appelle la fonction. Toute fonction se termine par une instruction de retour, qui permet au processeur de revenir là où il en était avant d'appeler la fonction. Certains processeurs disposent d'une instruction de retour dédiée, alors que d'autres l'émulent à partir d'autres instructions. L'instruction de retour a besoin de connaitre l'adresse de retour, l'adresse de la suite du programme. Celle-ci est sauvegardée soit par l'instruction d'appel de la fonction, soit par une instruction d'écriture spécialisée.

Principe des sous-programmes.

Sauvegarde de l'adresse de retour[modifier | modifier le wikicode]

Vu qu'une fonction apparait plusieurs fois dans notre programme, comment savoir à quelle instruction reprendre l'exécution de notre programme, une fois notre sous-programme terminé ? La seule solution est de sauvegarder l'adresse de l'instruction à laquelle il faut reprendre ! Le programme doit donc reprendre à l'instruction qui est juste après le branchement d'appel de fonction. Pour cela, on doit sauvegarder cette adresse appelée l'adresse de retour. Cette sauvegarde peut être faite de deux manières Il arrive que le processeur possède une instruction spéciale, capable de sauvegarder l'adresse de retour et de brancher vers le sous-programme voulu en une seule instruction. Cette instruction spéciale, capable de sauvegarder automatiquement l'adresse de retour et de brancher vers le sous-programme, est appelée une instruction d'appel de fonction. Dans les autres cas, on doit émuler cette instruction avec une instruction qui sauvegarde l'adresse de retour, suivie d'un branchement inconditionnel vers le sous-programme.

Une fois le sous-programme fini, il suffit de charger l'adresse de retour dans le registre pointeur d'instruction pour reprendre l’exécution de notre programme principal là où il s'était arrêté pour laisser la place au sous-programme. Là encore, deux solutions sont possibles pour faire cela. Sur certains processeurs, cela est fait par l'instruction située à la fin du sous-programme, qu'on nomme instruction de retour. C'est un branchement inconditionnel. Cette instruction a pour mode d'adressage, l'adressage implicite (l'adresse vers laquelle brancher est placée au sommet de la pile, pas besoin de la préciser). Sur d'autres, cette instruction spéciale n'existe pas et il faut encore une fois l'émuler avec les moyens du bord. L'astuce consiste souvent à charger l'adresse de retour dans un registre et utiliser un branchement inconditionnel vers cette adresse.

Sauvegarde des registres[modifier | modifier le wikicode]

Lorsqu'un sous-programme s'exécute, il va utiliser certains registres qui sont souvent déjà utilisés par le programme principal. Pour éviter d'écraser le contenu des registres, on doit donc conserver une copie de ceux-ci dans la pile, une sauvegarde de ceux-ci. Une fois que le sous-programme a finit de s'exécuter, il remet les registres dans leur état original en chargeant la sauvegarde dans les registres adéquats. Ce qui fait que lorsqu'un sous-programme a fini son exécution, tous les registres du processeur sont reviennent à leur ancienne valeur : celle qu'ils avaient avant que le sous-programme ne s'exécute. Rien n'est effacé ! Cette sauvegarde peut être effectuées automatiquement par l'instruction d'appel de fonction, ou être émulée en logiciel. Plus rarement, certains processeurs ont une isntruction pour sauvegarder les registres du processeur.

Pile d'appel[modifier | modifier le wikicode]

Pour pouvoir exécuter plusieurs fonctions, le processeur contient une ou plusieurs piles d'appel. Sans cela, certaines fonctionnalités de nos langages de programmation actuels n'existeraient pas ! Pour les connaisseurs, cela signifierait qu'on ne pourrait pas utiliser de fonctions réentrantes ou de fonctions récursives. Mais je n'en dis pas plus : vous verrez ce que cela veut dire d'ici quelques chapitres.

Généralités sur la pile d'appel[modifier | modifier le wikicode]

Cette pile d'appel est une partie de la mémoire RAM, ou une mémoire séparée, qui a une particularité : on stocke les données à l'intérieur d'une certaine façon. Les données sont regroupées dans la pile dans ce qu'on appelle des cadres de pile, des espèces de blocs de mémoire de taille variable.

Pile d'appel et cadres de pile.

Mais ce qui différencie une pile d'une simple collection de morceaux de mémoire, c'est la façon dont les cadres de pile sont gérés : les cadres sont crée un par uns et sont placés les uns à la suite des autres dans la mémoire. C'est une première contrainte : on ne peut pas créer de cadres n'importe où dans la mémoire. On peut comparer l'organisation des cadres à une pile d'assiette : on peut parfaitement rajouter une assiette au sommet de la pile d'assiette, ou enlever celle qui est au sommet, mais on ne peut pas toucher aux autres assiettes. Sur la pile de notre ordinateur, c'est la même chose : on ne peut accéder qu'à la donnée située au sommet de la pile. Comme pour une pile d'assiette, on peut rajouter ou enlever le cadre au sommet de la pile, mais pas toucher aux cadres en dessous, ni les manipuler. Le nombre de manipulations possibles sur cette pile se résume donc à trois manipulations de base qu'on peut combiner pour créer des manipulations plus complexes. On peut ainsi :

  • détruire le cadre de pile au sommet de la pile, et supprimer tout son contenu de la mémoire : on dépile.
  • créer un cadre de pile immédiatement après le dernier cadre de pile existante : on empile.
  • utiliser les données stockées dans le cadre de pile au sommet de la pile.
Primitives de gestion d'une pile.

Si vous regardez bien, vous remarquerez que la donnée au sommet de la pile est la dernière donnée à avoir été ajoutée (empilée) sur la pile. Ce sera aussi la prochaine donnée à être dépilée (si on n'empile pas de données au dessus). Ainsi, on sait que dans cette pile, les données sont dépilées dans l'ordre inverse d'empilement. Ainsi, la donnée au sommet de la pile est celle qui a été ajoutée le plus récemment.

Stack (data structure) LIFO.

Au fait, la pile peut contenir un nombre maximal de cadres, ce qui peut poser certains problèmes. Si l'on souhaite utiliser plus de stack frames que possible, il se produit un stack overflow, appelé en français débordement de pile. En clair, l'ordinateur plante !

Délimitation des cadres de pile[modifier | modifier le wikicode]

Pour localiser une donnée dans un cadre de pile, on utilise sa position par rapport au début ou la fin du cadre de pile : on peut donc calculer l'adresse de la donnée en additionnant cette position avec le contenu du stack pointer. Pour gérer ces piles, on a besoin de sauvegarder deux choses : l'adresse à laquelle commence le cadre de pile en mémoire, et de quoi connaitre l'adresse de fin. Et il existe diverses façons de faire. Pour ce faire, on a besoin d'un registre qui indique où est le sommet de la pile, quelle est son adresse. Ce registre est appelé le pointeur de sommet, ou encore le Stack Pointer (SP). A ce registre, on peut rajouter un autre registre qui sert à donner l'adresse de début du cadre de pile : le Frame Pointer (FP), ou pointeur de contexte. Certains processeurs possèdent deux registres spécialisés qui servent respectivement de pointeur de contexte et de Frame Pointer : on ne peut pas les utiliser pour autre chose. Si ce n'est pas le cas, on est obligé de stocker ces informations dans deux registres normaux, et se débrouiller avec les registres restants.

Frame pointer.

D'autres processeurs arrivent à se passer de Frame Pointer. Ceux-ci n'utilisent pas de registres pour stocker l'adresse de la base du cadre, mais préfèrent calculer cette adresse à partir de l'adresse de fin du cadre et de sa longueur. Cette longueur peut être stockée directement dans certaines instructions censées manipuler la pile : si chaque cadre a toujours la même taille, cette solution est clairement la meilleure. Cette solution est idéale si le cadre de pile a toujours la même taille. Mais il arrive que les cadres aie une taille qui ne soit pas constante : dans ce cas, on a deux solutions : soit stocker cette taille dans un registres, soit la stocker dans les instructions qui manipulent la pile, soit utiliser du code automodifiant.

Piles d'appel séparées contre pile d'appel unifiée[modifier | modifier le wikicode]

Pour exécuter plusieurs fonctions imbriquées les unes dans les autres, on doit sauvegarder plusieurs adresses de retour dans l'ordre. Pour cela, on utilise une pile d'adresses de retour. L'adresse de retour de la fonction en cours est toujours située au sommet de la pile. Certains processeurs émulent cette pile avec des registres généraux, ou sauvegardent une partie de la pile dans des registres cachés au lieu de tout mémoriser en RAM. La sauvegarde des registres peut aussi utiliser la pile d'appel, si ce n'est avoir une pile de sauvegarde des registres dédiée. Enfin, le processeur dispose souvent d'une autre pile, souvent fusionnée avec les deux précédentes, qui contient des données utiles pour chaque fonction.

Par exemple, certaines valeurs calculées hors de la fonction lui sont communiquées : ce sont des arguments ou paramètres. On peut communiquer ces valeurs de deux manières, soit en en fournissant une copie, soit en fournissant leur adresse. Dans les deux cas, cette transmission peut se faire par copie soit sur la pile, soit dans les registres. Dans le cas d'un passage par les registres, les registres qui contiennent les paramètres ne sont pas sauvegardés lors de l'appel de la fonction. Généralement, le passage par la pile est très utilisé sur les processeurs avec peu de registres, alors que les processeurs avec beaucoup de registres privilégient le passage par les registres.

De même, la fonction peut calculer un ou plusieurs résultats, qui sont récupérés par le programme principal et seront utilisés dans divers calculs. Ces résultats sont appelés des valeurs de retour. Généralement, c'est le programmeur qui décide de conserver une donnée et d'en faire une valeur de retour. Celui-ci peut avoir besoin de conserver le résultat d'un calcul pour pouvoir l'utiliser plus tard, par exemple. Ce résultat dépend fortement du sous-programme, et peut être n'importe quelle donnée : nombre entier, nombre flottant, tableau, objet, ou pire encore. Cette donnée, après avoir été "calculée" par le sous-programme, devra être conservée quelque part. Il va de soit que l'on ne peut pas stocker cette valeur de retour dans un registre : elle serait écrasée lors de la restauration des registres. Sans compter que cette valeur ne tient pas toujours dans un registre : un registre contenant 64 bits pourra difficilement contenir une valeur de retour de 5 kilo-octets. Pour éviter cela, on peut dédier certains registres à notre valeur de retour, et on se débrouille pour que la restauration des registres ne touche par ceux-ci. Mais un autre solution consiste à stocker ces valeurs de retour dans une pile dédiée : la pile des valeurs de retour. Ainsi, la valeur de retour est présente au sommet de la pile et peut être utilisée si besoin.

De plus, une fonction peut calculer des données temporaires, souvent appelées des variables locales. Une solution pour gérer ces variables consiste à réserver une portion de la mémoire statique pour chaque, dédiée au stockage de ces variables locales, mais cela gère mal le cas où une fonction s'appelle elle-même (fonctions récursives). Une autre solution est de réserver un cadre de pile pour les variables locales. Le processeur dispose de modes d'adressages spécialisés pour adresser les variables automatiques d'un cadre de pile. Ces derniers ajoutent une constante au stack pointer.

Pour gérer les fonctions, certains processeurs possèdent deux ou plusieurs piles spécialisées pour les adresses de retour, les registres à sauvegarder, les paramètres et les variables locales. Mais d'autres processeurs ne possèdent qu'une seule pile à la fois pour les adresses de retour, les variables locales, les paramètres, et le reste. Pour cela, il faut utiliser une pile avec des cadres de pile de taille variable, dans lesquels on peut ranger plusieurs données. Les variables locales sont souvent regroupées, de même que les arguments, le reste étant placé à une position bien précise dans le cadre de pile.

Pile exécution contenant deux cadres de pile, un pour la fonction drawLine() et un autre pour la fonction drawSquare(). Le bloc d'activation correspond grosso-modo au cadre de pile, auquel on ajoute les arguments (non-compris dans le cadre de pile dans cet exemple, chose plutôt rare).

Résumé[modifier | modifier le wikicode]

Instruction Utilité
Instructions arithmétiques Ces instructions font simplement des calculs sur des nombres. On peut citer par exemple :
  • L'addition ;
  • la multiplication ;
  • la division ;
  • le modulo ;
  • la soustraction ;
  • la racine carrée ;
  • le cosinus ;
  • et parfois d'autres.
Instructions logiques Elles travaillent sur des bits ou des groupes de bits. On peut citer :
  • Le ET logique.
  • Le OU logique.
  • Le XOR.
  • Le NON , qui inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1.
  • Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus.
  • Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre.
Instructions de test Elles peuvent comparer deux nombres entre eux pour savoir si une condition est remplie ou pas.

Pour citer quelques exemples, il existe certaines instructions qui peuvent vérifier si :

  • deux nombres sont égaux ;
  • si deux nombres sont différents ;
  • si un nombre est supérieur à un autre ;
  • si un nombre est inférieur à un autre.
Instructions de contrôle (branchements) Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elle permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses.
Instructions d’accès mémoire Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage.
Instructions de manipulation de chaines de caractères Certains processeurs intègrent des instructions capables de manipuler ces chaines de caractères directement. Mais autant être franc : ceux-ci sont très rares. Dans notre ordinateur, une lettre est stockée sous la forme d'un nombre souvent codé sur 1 octet (rappelez-vous le premier chapitre sur la table ASCII). Pour stocker du texte, on utilise souvent ce que l'on appelle des chaines de caractères : ce ne sont rien de plus que des suites de lettres stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte.
Les inclassables Il existe une grande quantité d'autres instructions, qui sont fournies par certains processeurs pour des besoins spécifiques.
  • Ainsi, certains processeurs ont des instructions spécialement adaptés aux besoins des OS modernes.
  • D'autres permettent de modifier la consommation en électricité de l'ordinateur (instructions de mise en veille du PC, par exemple).
  • Il arrive aussi qu'on puisse trouver des instructions qui permettent à des programmes de partager des données, d'échanger des informations (via Message Passing), etc. etc.
  • On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe.
  • De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires.
  • Certains processeurs sont aussi capables d'effectuer des instructions sur des structures de données assez complexes, comme des listes chainées ou des arbres.
  • Et on peut trouver bien d'autres exemples...