Fonctionnement d'un ordinateur/L'encodage des instructions

Un livre de Wikilivres.
Aller à la navigation Aller à la recherche

Pour rappel, les programmes informatiques exécutés par le processeur sont placés en mémoire RAM, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire sous la forme de suites de bits, tout comme les données. La seule différence est que les instructions sont chargées via le program counter, alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. Mais il est très rare que le processeur charge et exécute des données, qu'il prend par erreur pour des instructions. Cela demanderait en effet que le program counter ne fasse pas ce qui est demandé, ou que le programme exécuté soit bugué, voire qu'il soit conçu pour.

Toujours est-il qu'une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la taille de l'instruction. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement.

Un bon exemple de processeur à instruction de longueur variable est celui du jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes. Cela expliquent qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. Un autre exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. N'en parlons pas plus, l'encodage de ces instructions est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres !

L'opcode et l'encodage des modes d'adressage[modifier | modifier le wikicode]

Une instruction n'est pas encodée n'importe comment et la suite de bits associée a une certaine structure. Quelques bits de l’instruction indiquent quelle est l'opération à effectuer : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Cette portion de mémoire s'appelle l'opcode. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. De manière générale, on peut dire qu'il existe autant d'opcode que d'instructions pour un processeur. Évidemment, qui dit beaucoup d'opcodes dit processeur plus complexe : les circuits de gestion des opcodes sont naturellement plus complexes quand ces opcodes sont nombreuses. Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes.

Il arrive que certaines instructions soient composées d'un opcode, sans rien d'autre : elles ont alors une représentation en binaire qui est unique. Mais certaines instructions ajoutent une partie variable, pour préciser la localisation des données à manipuler. Une instruction peut alors fournir au processeur ce qu'on appelle une référence, à savoir quelque chose qui permet de localiser une donnée dans la mémoire. Cette référence pourra ainsi préciser plus ou moins explicitement dans quel registre, à quelle adresse mémoire, à quel endroit sur le disque dur, se situe la donnée à manipuler. Elles indiquent où se situent les opérandes d'un calcul, où stocker son résultat, où se situe la donnée à lire ou écrire, à quel l'endroit brancher. Il faut préciser que toutes les références n'ont pas la même taille : une adresse utilisera plus de bits qu'un nom de registres, par exemple (il y a moins de registres que d'adresses). Par exemple, une instruction de calcul dont les deux références sont des adresse mémoire prendra plus de place qu'un calcul qui manipule deux registres.

Reste à savoir quelle est la nature de la référence : est-ce une adresse, un nombre, un nom de registre ? Chaque manière d’interpréter la partie variable s'appellent un mode d'adressage. Pour résumer, un mode d'adressage indique au processeur que telle référence est une adresse, un registre, autre chose. Il est possible qu'une instruction précise plusieurs références, qui sont chacune soit une adresse, soit une donnée, soit un registre. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser une opérande pour chaque (dans le pire des cas). Les instructions manipulant plusieurs références peuvent parfois utiliser un mode d'adressage différent pour chaque. Comme nous allons le voir, certaines instructions supportent certains modes d'adressage et pas d'autres. Généralement, les instructions d'accès mémoire possèdent plus de modes d'adressage que les autres, encore que cela dépende du processeur (chose que nous détaillerons dans le chapitre suivant).

Il existe deux méthodes pour préciser le mode d'adressage utilisé par l'instruction. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, toutes les instructions arithmétiques ne peuvent manipuler que des registres. Dans un cas pareil, pas besoin de préciser le mode d'adressage, qui est déduit automatiquement via l'opcode: on parle de mode d'adressage implicite. Dans certains cas, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de mode d'adressage explicite. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal.

Exemple d'une instruction avec mode d'adressage explicite.

Les modes d'adressages pour les données[modifier | modifier le wikicode]

Pour comprendre un peu mieux ce qu'est un mode d'adressage, voyons quelques exemples de modes d'adressages assez communs et qui reviennent souvent. Nous allons commencer par aborder l'adressage des données et les modes d'adressages qui correspondent.

L'adressage implicite[modifier | modifier le wikicode]

Avec l'adressage implicite, la partie variable n'existe pas ! Il peut y avoir plusieurs raisons à cela. Il se peut que l'instruction n'ait pas besoin de données : une instruction de mise en veille de l'ordinateur, par exemple. Ensuite, certaines instructions n'ont pas besoin qu'on leur donne la localisation des données d'entrée et « savent » où sont les données. Comme exemple, on pourrait citer une instruction qui met tous les bits du registre d'état à zéro. Certaines instructions manipulant la pile sont adressées de cette manière : on sait d'avance dans quels registres sont stockées l'adresse de la base ou du sommet de la pile.

L'adressage immédiat[modifier | modifier le wikicode]

Avec l'adressage immédiat, la partie variable est une constante : un nombre entier, un caractère, un nombre flottant, etc. Avec ce mode d'adressage, la donnée est placée dans la partie variable et est chargée en même temps que l'instruction. Ces constantes sont souvent codées sur 8 ou 16 bits : aller au-delà serait inutile vu que la quasi-totalité des constantes manipulées par des opérations arithmétiques sont très petites et tiennent dans un ou deux octets.

Adressage immédiat

L'adressage direct[modifier | modifier le wikicode]

Passons maintenant à l'adressage absolu, aussi appelé adressage direct. Avec lui, la partie variable est l'adresse de la donnée à laquelle accéder. Cela permet de lire une donnée directement depuis la mémoire sans devoir la copier dans un registre.

Adressage direct

L'adressage inhérent[modifier | modifier le wikicode]

Avec le mode d'adressage inhérent, la partie variable va identifier un registre qui contient la donnée voulue. Ce mode d'adressage demande d'attribuer un nom de registre à chaque registre. Pour rappel, ce dernier est un numéro attribué à chaque registre, utilisé pour préciser à quel registre le processeur doit accéder.

Adressage inhérent

L'adressage indirect à registre[modifier | modifier le wikicode]

Dans certains cas, les registres généraux du processeur peuvent stocker des adresses mémoire. On peut alors décider d'accéder à l'adresse qui est stockée dans un registre : c'est le rôle du mode d'adressage indirect à registre. Ici, la partie variable identifie un registre contenant l'adresse de la donnée voulue. La différence avec le mode d'adressage inhérent vient de ce qu'on fait de ce nom de registre : avec le mode d'adressage inhérent, le registre indiqué dans l'instruction contiendra la donnée à manipuler, alors qu'avec le mode d'adressage indirect à registre, le registre contiendra l'adresse de la donnée.

Adressage indirects à registre

Ce mode d'adressage indirect à registre permet d'implémenter de façon simple ce qu'on appelle les pointeurs. Il s'agit de fonctionnalités de certains langages de programmation dits bas-niveau (proches du matériel), dont le C. Les pointeurs sont des variables dont le contenu est une adresse mémoire. Cette définition est certes simple, mais beaucoup d'étudiants la trouve très abstraite et ne voient pas à quoi ces variables peuvent servir. Dans les grandes lignes, ils servent dès que l'on manipule/crée des structures de données, peu importe le langage de programmation utilisé. C'est explicite dans des langages comme le C, mais implicite dans les langages haut-niveau. Manipuler des tableaux, des listes chainées, des arbres, ou tout autre structure de donnée un peu complexe, se fait à grand coup de pointeurs. C'est surtout le cas dans les structures de données où les données sont dispersées dans la mémoire, comme les listes chaînées, les arbres, et toute structure éparse. Localiser les données en question dans la mémoire demande d'utiliser des pointeurs qui pointent vers ces données, qui donnent leur adresse.

Pointeurs.

L'adressage indirect à registre avec auto-incrément[modifier | modifier le wikicode]

Pour faciliter ces parcours de tableaux, on a inventé les modes d'adressages indirect avec auto-incrément (register indirect autoincrement) et indirect avec auto-décrément (register indirect autodecrement), des variantes du mode d'adressage indirect qui augmentent ou diminuent le contenu du registre d'une valeur fixe automatiquement. Cela permet de passer directement à l’élément suivant ou précédent dans un tableau. Il existe une variante où l'incrémentation ou la décrémentation s'effectuent avant l'utilisation effective de l'adresse.

Adressage indirect à registre post-incrémenté

L'adressage indexed absolute[modifier | modifier le wikicode]

D'autres modes d'adressage permettent de faciliter le calcul de l'adresse d'un élément du tableau. Pour éviter d'avoir à calculer les adresses à la main avec le mode d'adressage indirect à registre, on a inventé un mode d'adressage pour combler ce manque : le mode d'adressage absolu indexé (indexed absolute). Celui-ci fournit l'adresse de base du tableau, et un registre qui contient l'indice. À partir de ces deux données, l'adresse de l’élément du tableau est calculée, envoyée sur le bus d'adresse, et l’élément est récupéré.

Indexed Absolute

Ce mode d'adressage indexed absolute ne marche que pour des tableaux dont l'adresse est fixée une bonne fois pour toute. Ces tableaux sont assez rares : ils correspondent aux tableaux de taille fixe, déclarée dans la mémoire statique (souvenez-vous de la section précédente).

L'adressage base + index[modifier | modifier le wikicode]

La majorité des tableaux sont des tableaux dont l'adresse n'est pas connue lors de la création du programme : ils sont déclarés sur la pile ou dans le tas, et leur adresse varie à chaque exécution du programme. On peut certes régler ce problème en utilisant du code automodifiant, mais ce serait vendre son âme au diable ! Pour contourner les limitations du mode d'adressage indexed absolute, on a inventé le mode d'adressage base + index.

Avec ce dernier, l'adresse du début du tableau n'est pas stockée dans l'instruction elle-même, mais dans un registre. Elle peut donc varier autant qu'on veut. Ce mode d'adressage spécifie deux registres dans sa partie variable : un registre qui contient l'adresse de départ du tableau en mémoire, le registre de base, et un qui contient l'indice, le registre d'index. Le processeur calcule alors l'adresse de l’élément voulu à partir du contenu de ces deux registres, et accède à notre élément. En clair : notre instruction ne fait pas que calculer l'adresse de l’élément : elle va aussi le lire ou l'écrire.

Base + Index

Ce mode d'adressage possède une variante qui permet de vérifier qu'on ne « déborde » pas du tableau, en calculant par erreur une adresse en dehors du tableau, à cause d'un indice erroné, par exemple. Accéder à l’élément 25 d'un tableau de seulement 5 éléments n'a pas de sens et est souvent signe d'une erreur. Pour cela, l'instruction peut prendre deux opérandes supplémentaires (qui peuvent être constants ou placés dans deux registres). L'instruction BOUND sur le jeu d'instruction x86 en est un exemple. Si cette variante n'est pas supportée, on doit faire ces vérifications à la main.

L'adressage base + décalage[modifier | modifier le wikicode]

Outre les tableaux, les programmeurs utilisent souvent ce qu'on appelle des structures. Ces structures servent à créer des données plus complexes que celles que le processeur peut supporter. Mais le processeur ne peut pas manipuler ces structures : il est obligé de manipuler les données élémentaires qui la constituent une par une. Pour cela, il doit calculer leur adresse, ce qui n'est pas très compliqué. Une donnée a une place prédéterminée dans une structure : elle est donc a une distance fixe du début de celle-ci.

Calculer l'adresse d'un élément de notre structure se fait donc en ajoutant une constante à l'adresse de départ de la structure. Et c'est ce que fait le mode d'adressage base + décalage. Celui-ci spécifie un registre qui contient l'adresse du début de la structure, et une constante. Ce mode d'adressage va non seulement effectuer ce calcul, mais il va aussi aller lire (ou écrire) la donnée adressée.

Base + offset

L'adressage base + index + décalage[modifier | modifier le wikicode]

Certains processeurs vont encore plus loin : ils sont capables de gérer des tableaux de structures ! Ce genre de prouesse est possible grâce au mode d'adressage base + index + décalage. Avec ce mode d'adressage, on peut calculer l'adresse d'une donnée placée dans un tableau de structure assez simplement : on calcule d'abord l'adresse du début de la structure avec le mode d'adressage base + index, et ensuite on ajoute une constante pour repérer la donnée dans la structure. Et le tout, en un seul mode d'adressage.

Les modes d'adressage pour les branchements[modifier | modifier le wikicode]

Les modes d'adressage des branchements permettent de donner l'adresse de destination du branchement, l'adresse vers laquelle le processeur reprend son exécution si le branchement est pris. Les instructions de branchement peuvent avoir plusieurs modes d'adressages : implicite, direct, relatif ou indirect. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit dans un registre du processeur (branchement indirect), soit calculée à l’exécution (relatif), soit précisée de manière implicite (retour de fonction, adresse sur la pile).

Les branchements implicites[modifier | modifier le wikicode]

Les branchements implicites se limitent aux instructions de retour de fonction, où l'adresse de destination est située au sommet de la pile d'appel.

Les branchements directs[modifier | modifier le wikicode]

Avec un branchement direct, l'opérande est simplement l'adresse de l'instruction à laquelle on souhaite reprendre.

Branchement direct.

Les branchements relatifs[modifier | modifier le wikicode]

Les branchements relatifs permettent de localiser la destination d'un branchement par rapport à l'instruction en cours. Cela permet de dire « le branchement est 50 instructions plus loin ». Avec eux, l'opérande est un nombre qu'il faut ajouter au registre d'adresse d'instruction pour tomber sur l'adresse voulue. On appelle ce nombre un décalage (offset).

Branchement relatif

Les branchements indirects[modifier | modifier le wikicode]

Avec les branchements indirects, l'adresse vers laquelle on souhaite brancher peut varier au cours de l’exécution du programme. Ces branchements sont souvent camouflés dans des fonctionnalités un peu plus complexes des langages de programmation (pointeurs sur fonction, chargement dynamique de bibliothèque, structure de contrôle switch, et ainsi de suite). Avec ces branchements, l'adresse vers laquelle on veut brancher est stockée dans un registre.

Branchement indirect