# Fonctionnement d'un ordinateur/L'encodage des instructions

Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. 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. 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, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données.

## La taille d'une instruction : taille fixe ou variable

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. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres !
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.
Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est 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.

## L'encodage d'une instruction : généralités

Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en champs, à savoir des suites de bits qui ont une signification.

### L'opcode, aussi appelé "code opération"

Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc.
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.
En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre.
Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique.
    Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique.

### L'encodage des opérandes et des références

En plus de l'opcode, une instruction doit préciser quels sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode.
Le second cas ne fournit pas l'opérande, mais une référence qui indique où se trouve l'opérande. Elle précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. L'opérande est associé à un mode d'adressage qui précise comment interpréter la référence, si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer une adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence.
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. En conséquence, 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. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire.
Un problème est que les instructions n'ont pas le même nombre d'opérande. Les instructions arithmétiques et logiques sont souvent des instructions dyadiques, à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares instructions monadiques qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même.
Ne parlons pas des opérations d'accès mémoire qui n'ont ni opérande ni résultat. Elles copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes.
Les instructions de longueur variables permettent de résoudre ce problème de longueur. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou pour encoder sa source et de sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Mais les instructions de longueur fixe se débrouillent autrement. Elles se calent sur le pire des cas, l'instruction la plus longue, et doivent trouver un moyen de tout faire rentrer dans 16, 32 ou 64 bits. Généralement, elles réduisent les possibilités d'adressage, certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, etc. Leur encodage est donc soit plus compliqué, soit plus restrictif.
Une solution souvent utilisée réduit la taille des opérations dyadiques. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d'encodage 3-références, plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons.
Mais encoder 2 opérandes + 1 résultat prend de la place. L'encodage 3-référence peut être modifié de manière à adresser le résultat de manière implicite. Pour économiser un peu de place, l'idée est d'adresser le résultat de manière implicite. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d'encodage 2-références, d'encodage 2-adresses, ou encore d'encodage 2-opérandes.
Un exemple d'utilisation est celui des processeurs RISC-V, qu'on a mentionné plus haut. Nous avions dit qu'ils géraient des instructions 32 bits, et des instructions compressées de 16 bits. 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 et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois.
L'encodage 2-références peut aussi être utilisé avec l'adressage immédiat. Une instruction peut l'utiliser si un opérande est en adressage immédiat, les deux autres sont dans des registres. La raison est qu'une constante prend approximativement autant de bits qu'une adresse. Les deux sont souvent codées sur 16 bits, plus rarement 20/24 bits. Avec des instructions codées sur 32 bits, cela ne laisse qu'un ou deux octets pour coder l'opcode et les registres. L'encodage 2-opérande est alors une nécessité, économiser un numéro de registre est le seul moyen d'avoir assez de place si la constante est codée sur 20/24 bits.
Les instructions monadiques utilisent pas défaut l'encodage 2-références, pour préciser l'opérande et le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte.

### L'encodage du mode d'adressage

Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire.
Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (Reduced Instruction Set Computer) , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de mode d'adressage implicite.
Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite.
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.
Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat.
Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat.
Encodage d'une instruction où chaque opérande/résultat est adressée explicitement :
| Opcode | Mode d'adressage opérande 1 | Référence vers l'opérande 1 | Mode d'adressage opérande 2 | Référence vers l'opérande 2 | Mode d'adressage résultat | Référence vers le résultat |
L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable.
Encodage d'une instruction où chaque opérande/résultat est adressée explicitement :
| Opcode | Mode d'adressage opérande 1 | Mode d'adressage opérande 2 | Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |
Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place.
Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement :
| Opcode | Mode d'adressage opérande 1 | Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |
Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit.

### L'encodage de la taille des opérandes

Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits.
Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. A la place, il contient deux bits qui sont à interpréter comme suit :
* 00 pour des données de 8 bits ;
* 01 pour des données de 16 bits ;
* 10 pour des données de 32 bits ;
* 11 pour des données de 64 bits.
En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'alias de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort.
Mais quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres.

## Les restrictions sur les modes d'adressage

Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement.

### La gestion du mode d'adressage immédiat

Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois.
De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire.

### Les restrictions de registres source/destination

Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples.
Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort.
Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX.
De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate.
Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers.

### Les restrictions sur les opérandes mémoire

Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres.
Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles.
Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d'instruction simple-accès pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des instructions multi-accès. Et nous allons les voir séparément.

## L'encodage des instructions simple-accès

Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès.

### Les instructions mémoire simple-accès et leur encodage

Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont :
* les instructions LOAD pour charger une donnée de la RAM dans un registre ;
* l'instruction STORE pour copier une donnée d'un registre vers la RAM ;
* l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ;
* l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination.
D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté.
Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions.
Instruction LOAD :
| Opcode | Adressage de la source Adressage absolu (adresse) Adressage indirect à registre Adressage base + indice etc. | Adresse ou référence de la source | | Nom de registre de destination |
| Instruction STORE |||||||
| Opcode | | Nom de registre source | Adressage de la destination Adressage absolu (adresse) Adressage indirect à registre Adressage base + indice etc. | Adresse ou référence de destination |
| Instruction MOV |||||||
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) | Nom de registre source | | Nom de registre destination |
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) | Constante immédiate | | Nom de registre destination |
Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre.
Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination.
Encodage d'une instruction MOV généraliste :
| Opcode | Bit qui indique l'ordre des champs | Nom de registre source/destination | Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |
| Opcode | Bit qui indique l'ordre des champs | Nom de registre source/destination | Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) | Adresse mémoire ou registres pour les pointeurs |
| Opcode | Bit qui indique l'ordre des champs | Nom de registre source/destination | Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) | Constante immédiate |

### Les instructions dyadiques simple-accès et leur encodage

Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas.
Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant.
Dans ce qui suit, nous parlerons d'instructions load-op pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux instructions reg-reg, ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les instructions cst-reg, où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent.
Encodage d'une instruction dyadique reg-reg :
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | Nom de registre pour l'opérande 2 | Nom de registre pour le résultat |
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) | Constante immédiate | Nom de registre pour l'opérande 2 | Nom de registre pour le résultat |
Encodage d'une instruction load-op :
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) Adressages mémoire (adresse, pointeur, autre) | Constante immédiate | Nom de registre destination |
| Opcode | Adressage inhérent (registre) Adressage immédiat (constante) Adressages mémoire (adresse, pointeur, autre) | Adresse mémoire ou registres pour les pointeurs | Nom de registre destination |
Encoder une instruction load-op est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions load-op, ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes.

## Les instructions multi-accès et leur encodage

Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre.

### Les instructions mémoire-mémoire

Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d'instruction mémoire-mémoire, ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire.
Les instructions mémoire-mémoire les plus communes sont les instructions de copie mémoire, où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation.
Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination.
Encodage d'une instruction de copie mémoire :
| Opcode | Mode d'adressage de la source Adressage absolu (adresse) Adressages mémoire indirect à registre (pointeur) Adressages mémoire base + indice etc. | Mode d'adressage de la destination Adressage absolu (adresse) Adressages mémoire indirect à registre (pointeur) Adressages mémoire base + indice etc. | Référence(s) vers la source | Référence(s) vers la destination |
Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits.
Encodage d'une instruction de copie mémoire :
| Opcode | Mode d'adressage de la source et de la destination Adressage absolu (adresse) Adressages mémoire indirect à registre (pointeur) Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |
Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées.
Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une instruction mémoire généraliste, souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination.
* Si les deux opérandes sont des registres, le premier registre est copié dans le second.
* Si le premier opérande est un registre et l'autre une adresse, c'est une écriture.
* Si le premier opérande est une adresse et l'autre un registre, c'est une lecture.
* Si les deux opérandes sont des adresses, c'est une copie mémoire.
Encodage d'une instruction mémoire généraliste :
| Opcode | Mode d'adressage de la source Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) Adressage immédiat (constante) | Mode d'adressage de la destination Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |
| Opcode | Mode d'adressage de la source Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) Adressage immédiat (constante) | Mode d'adressage de la destination Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |
| Opcode | Mode d'adressage de la source Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) Adressage immédiat (constante) | Mode d'adressage de la destination Adressage inhérent (registre) Adressages mémoire (adresse, pointeur, autre) | Constante immédiate |

### Les instructions multi-accès monadiques et dyadiques

Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques.
Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées instructions read-modify-write. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques.
Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs.
Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable.

## Le cas des processeurs x86

Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et load-op, mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général.

### L'encodage d'une instruction x86

L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes.
Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs.
Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M.
Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un offset, pour les adressages base + offset et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + offset. Ce n'est pas très courant.
| Préfixes | opcode | Mod R/M | SIB | Displacement | Immediate |
| Préfixes | opcode | Mode d'adressage | registres pour l'adressage base + indice | offset | Constante immédiate |
| facultatif | obligatoire | facultatif | facultatif | facultatif |
| 0 à 4 octets | 1, 2, 3 octets, souvent un seul | 1 octet | 1 octet | 1, 2 ou 4 octets | 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |
L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base.
Octet SIB :
| Taille de l'opérande | Base | Indice |
| Entier | Numéro de registre | Numéro de registre |
| 2 bits | 3 bits | 3 bits |
L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage :
* 00 pour l'adressage indirect à registre ou l'adressage base + indice.
* 01 pour dire qu'un offset de 1 octet est présent.
* 10 pour dire qu'un offset de 4 octets est présent.
* 11 si les deux opérandes sont dans un registre.
Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP.
Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande.
Octet SIB :
| Taille de l'opérande | Base | Indice |
| Entier | Numéro de registre | Numéro de registre |
| 2 bits | 3 bits | 3 bits |

### Les string instructions

Les instructions dont nous allons parler sont connues sous le nom d'instructions de chaines de caractère, bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises.
Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes :
* L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU.
* L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs.
Les autres instructions de chaine de caractère regroupent trois instructions :
* LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI.
* STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI.
* SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI.
Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant.
Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées.
Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0.
D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies.