Fonctionnement d'un ordinateur/L'encodage des instructions
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. 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 de ces instructions 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. 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.
L'opcode et l'encodage des modes d'adressage
modifierUne 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.
Les modes d'adressages pour les données
modifierPour comprendre un peu mieux ce qu'est un mode d'adressage, nous allons voir les modes d'adressage les plus simples qui soient. Ils sont supportés par la majorité des processeurs existants, à quelques détails près que nous élaborerons dans le chapitre suivant. Il s'agit des modes d'adressage directs, qui permettent de localiser directement une donnée dans la mémoire ou dans les registres. Ils précisent dans quel registre, à quelle adresse mémoire se trouve une donnée.
L'adressage implicite
modifierAvec 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. Pareil pour les instructions manipulant la pile : on sait d'avance dans quels registres sont stockées l'adresse de la base ou du sommet de la pile.
L'adressage inhérent (à registre)
modifierAvec 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 numéro de registre à chaque registre, parfois appelé abusivement un nom de registre. Pour rappel, ce dernier est un numéro attribué à chaque registre, utilisé pour préciser à quel registre le processeur doit accéder. On parle aussi d'adressage à registre, pour simplifier.
L'adressage immédiat
modifierAvec 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.
Les constantes en adressage immédiat 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. La plupart du temps, les constantes sont des entiers signés, c'est à dire qui peuvent être positifs, nuls ou négatifs. Au vu de la différence de taille entre la constante et les registres, les constantes subissent une opération d'extension de signe avant d'être utilisées.
Pour rappel, l'extension de signe convertit un entier en un entier plus grand, codé sur plus de bits, tout en préservant son signe et sa valeur. L'extension de signe des nombres positifs consiste à remplir les bits de poids fort avec des 0 jusqu’à arriver à la taille voulue : c'est la même chose qu'en décimal, où rajouter des zéros à gauche d'un nombre positif ne changera pas sa valeur. Pour les nombres négatifs, il faut remplir les bits à gauche du nombre à convertir avec des 1, jusqu'à obtenir le bon nombre de bits : par exemple, 1000 0000 (-128 codé sur 8 bits) donnera 1111 1111 1000 000 après extension de signe sur 16 bits. L'extension de signe d'un nombre codé en complément à 2 se résume donc en une phrase : il faut recopier le bit de poids fort de notre nombre à convertir à gauche de celui-ci jusqu’à atteindre le nombre de bits voulu.
L'adressage absolu
modifierPassons 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 RAM/ROM. Le terme "adressage par adresse" est aussi utilisé. Un défaut de ce mode d'adressage est que l'adresse en question a une taille assez importante, elle augmente drastiquement la taille de l'instruction. Les instructions sont donc soit très longues, sans optimisations.
Pour raccourcir les instructions, il est possible de ne pas mettre des adresses complètes, mais de retirer les bits de poids forts. L'adressage absolu ne peut alors lire qu'une partie de la mémoire RAM. Il est aussi possible de ne pas encoder les bits de poids faible pour des questions d'alignement mémoire. Les processeurs RISC modernes gèrent parfois le mode d'adressage absolu, ils encodent des adresses sur 16-20 bits pour des processeurs 32 bits. Un exemple plus ancien est le cas de l’ordinateur Data General Nova. Son processeur était un processeur 16 bits, capable d'adresser 64 kibioctets. Il gérait plusieurs modes d'adressages, dont un mode d'adressage absolu avec des adresses codées sur 8 bits. En conséquence, il était impossible d’accéder à plus de 256 octets avec l'adressage absolu, il fallait utiliser d'autres modes d'adressage pour cela. Il s'agit d'un cas extrême.
Une solution un peu différente des précédentes utilise des adresses de taille variable, et donc des instructions de taille variable. Un exemple est celui du mode zero page des processeurs Motorola, notamment des Motorola 6800 et des MOS Technology 6502. Sur ces processeurs, il y avait deux types d'adressages absolus. Le premier mode utilisait des adresses complètes de 16 bits, capables d'adresser toute la mémoire, tout l'espace d'adressage. Le second mode utilisait des adresses de 8 bits, et ne permettait que d'adresser les premiers 256 octets de la mémoire. L'instruction était alors plus courte : avec un opcode de 8bits et des adresses de 8 bits, elle rentrait dans 16 bits, contre 24 avec des adresses de 16 bits. Un autre avantage était que l'accès à ces 256 octets était plus rapide d'un cycle d'horloge, ce qui fait qu'ils étaient monopolisés par le système d'exploitation et les programmes utilisateurs, mais ce n'est pas lié au mode d'adressage absolu proprement dit.
Les modes d'adressage indirects pour les pointeurs
modifierLes modes d'adressages précédents sont appelés les modes d'adressage directs car ils fournissent directement une référence vers la donnée, en précisant dans quel registre ou adresse mémoire se trouve la donnée. Les modes d'adressage qui vont suivre ne sont pas dans ce cas, ils permettent de localiser une donnée de manière indirecte, en passant par un intermédiaire. D'où leurs noms de modes d'adressage indirects. L'intermédiaire en question est ce qui s'appelle un pointeur. 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. En clair, les modes d'adressage indirects ne disent pas où se trouve la donnée, mais où se trouve l'adresse de la donnée, un pointeur vers celle-ci.
Pour rendre le tout plus intuitif, sachez qu'on a déjà rencontré un cas de ce genre. En effet, les registres de pile sont déjà en soi une forme d'adressage indirect : ils mémorisent une adresse mémoire, pas une donnée ! D'ailleurs, ce n'est pas pour rien que le registre pointeur de pile s'appelle ainsi ! Le pointeur de pile indique la position du sommet de la pile en mémoire RAM, il stocke l'adresse de la donnée au sommet de la pile, c'est un pointeur vers le sommet de la pile. Et ce pointeur de pile est stocké dans un registre, adressé implicitement ou explicitement.
L'utilité des pointeurs : les structures de données
modifierLes pointeurs ont une définition très simple, mais beaucoup d'étudiants la trouve très abstraite et ne voient pas à quoi ces pointeurs peuvent servir. Pour résumer rapidement, les pointeurs sont utilisées pour manipuler/créér des structures de données, à savoir des regroupements structurées de données plus simples, peu importe le langage de programmation utilisé. 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 explicite dans des langages comme le C, mais implicite dans les langages haut-niveau. 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.
Les structures de données les plus simples sont appelées "structures" ou enregistrements. Elles regroupent plusieurs données simples, comme des entiers, des adresses, des flottants, des caractères, etc. Par exemple, on peut regrouper deux entiers et un flottant dans une structure, qui regroupe les deux. Les données de la structure sont placées les unes à la suite des autres dans la RAM, à partir d'une adresse de début. Localiser une donnée dans la structure demande simplement de connaitre à combien de byte se situe la donnée par rapport à l'adresse de début. Une simple addition permet de calculer cette adresse, et des modes d'adressage permettent de faire ce calcul implicitement.
Un autre type de structure de donnée très utilisée est les tableaux, des structures de données où plusieurs données de même types sont placées les unes à la suite des autres en mémoire. Par exemple, on peut placer 105 entiers les uns à la suite des autres en mémoire. Toute donnée dans le tableau se voit attribuer un indice, un nombre entier qui indique la position de la donnée dans le tableau. Attention : les indices commencent à zéro, et non à 1, ce qui fait que la première donnée du tableau porte l'indice 0 ! L'indice dit si on veut la première donnée (indice 0), la deuxième (indice 1), la troisième (indice 2), etc.
Le tableau commence à une adresse appelée l'adresse de base, qui est mémorisée dans un pointeur. Localiser un entier dans le tableau demande de faire des calculs avec le pointeur et l'indice. Intuitivement, on se dit qu'il suffit d'additionner le pointeur avec l'indice. Mais ce serait oublier qu'il faut tenir compte de la taille de la donnée. Le calcul de l'adresse d'une donnée dans le tableau se fait en multipliant l'indice par la taille de la donnée, puis en additionnant le pointeur. De nombreux modes d'adressage permettent de faire ce calcul directement, comme nous allons le voir.
L'adressage indirect à registre pour les pointeurs
modifierLes modes d'adressage indirects sont des variantes des modes d'adressages directs. Par exemple, le mode d'adressage inhérent indique le registre qui contient la donnée, sa version indirecte indique le registre qui contient le pointeur, qui pointe vers une donnée en RAM/ROM. Idem avec le mode d'adressage absolu : sa version directe fournit l'adresse de la donnée, sa version indirecte fournit l'adresse du pointeur.
Par contre, il n'est pas possible de prendre tous les modes d'adressage précédents, et d'en faire des modes d'adressage indirects. L'adressage implicite reste de l'adressage implicite, peu importe qu'il adresse une donnée ou un pointeur (comme le pointeur de pile). Quand à l'adressage immédiat, il n'a pas d'équivalent indirect, même si on peut interpréter l'adressage absolu comme tel. Pour résumer, un pointeur peut être soit dans un registre, soit en mémoire RAM, ce qui donne deux classes de modes d'adressages indirect : à registre et mémoire. Nous allons d'abord voir l'adressage indirect à registre, ainsi que ses nombreuses variantes.
Avec l'adressage indirect à registre, le pointeur est stockée dans un registre. Le registre en question contient donc un l'adresse de la donnée à lire/écrire, celle qui pointe vers la donnée à lire/écrire. Lors de l'exécution de l'instruction, le pointeur dans le registre est envoyé sur le bus d'adresse, et la donnée est récupérée sur le bus de données.
Ici, la partie variable de l'instruction 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.
L'adressage indirect à registre gère les pointeurs nativement, mais pas plus. Il faut encore faire des calculs d'adresse pour gérer les tableaux ou les enregistrements, et ces calculs sont réalisés par des instructions de calcul normales. Le mode d'adressage indirect à registre ne gére pas de calculs d'adresse en lui-même. Et les modes d'adressages qui vont suivre intègrent ce mode de calcul directement dans le mode d'adressage ! Avec eux, le processeur fait le calcul d'adresse de lui-même, sans recourir à des instructions spécialisées. Sans ces modes d'adressage, utiliser des tableaux demande d'utiliser du code automodifiant ou d'autres méthodes qui relèvent de la sorcellerie.
Pour faciliter ces parcours de tableaux, il existe des variantes de l'adressage précédent, qui incrémentent ou décrémentent automatiquement le pointeur à chaque lecture/écriture. Il s'agit des modes d'adressages indirect avec auto-incrément (register indirect autoincrement) et indirect avec auto-décrément (register indirect autodecrement). Avec eux, le contenu du registre est incrémenté/décrémenté d'une valeur fixe automatiquement. Cela permet de passer directement à l’élément suivant ou précédent dans un tableau.
En théorie, il y a une différence entre les deux modes d'adressages. Avec l'adressage indirect avec auto-incrément, l'incrémentation se fait APRES l'envoi de l'adresse, après la lecture/écriture. On effectue l'accès mémoire avec le pointeur, avant d'incrémenter le pointeurs. Par contre, pour l'adresse indirect avec auto-décrément, on décrémente le pointeur AVANT de faire l'accès mémoire. Les deux comportements semblent incohérents, mais ils sont en réalité très intuitifs quand on sait comment se fait le parcours d'un tableau.
Le parcours d'un tableau du début vers la fin commence à l'adresse de base du tableau, celle de son premier élément. Aussi, si on place l'adresse de base du tableau dans un pointeur, on accède à l'adresse, puis ensuite on incrémente le tout. Pour le parcours en sens inverse, on commence à l'adresse de fin du tableau, celle à laquelle on quitte le tableau. Ce n'est pas l'adresse du dernier élément, mais l'adresse qui se situe immédiatement après. Pour obtenir l'adresse du dernier élément, on doit soustraire la taille de l'élément à l'adresse initiale. En clair, on décrémente l'adresse avant d'y accéder.
Les deux modes d'adressage posent des problèmes avec les exceptions matérielles. Le problème vient du fait que l'accès mémoire peut générer une exception matérielle, comme un problème de mémoire virtuelle ou autres. Dans ce cas, l'exception matérielle est gérée par une routine d'interruption, puis la routine se termine et l'instruction cause est ré-exécutée. Mais la ré-exécution doit tenir compte du fait que le pointeur initial a été incrémenté/décrémentée, et qu'il faut donc le faire revenir à sa valeur initiale. Quelques machines ont eu des problèmes d'implémentation de ce genre, notamment le DEC VAX et le Motorola 68000.
Les modes d'adressage indirects indicés pour les tableaux
modifierLe mode d'adressage base + indice est utilisé lors de l'accès à un tableau, quand on veut lire/écrire un élément de ce tableau. L'adressage base + indice, fournit à la fois l'adresse de base du tableau et l'indice de l’élément voulu. Les deux sont dans un registre, ce qui fait que ce mode d'adressage précise deux numéros/noms de registre. En clair, indice et pointeur sont localisés via adressage inhérent (à registre). Le calcul de l'adresse est effectué automatiquement par le processeur.
Il existe une variante qui permet de vérifier qu'on ne « déborde » pas du tableau, qu'on ne calcule pas 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.
Le mode d'adressage absolu indexé (indexed absolute, ou encore base+offset) est une variante de l'adressage précédent, qui est spécialisée pour les tableaux dont l'adresse de base est fixée une fois pour toute, elle est connue à la compilation. Les tableaux de ce genre sont assez rares : ils correspondent aux tableaux de taille fixe, déclarée dans la mémoire statique. L'adresse de base du tableau est alors précisée via une adresse mémoire et non un nom de registre. En clair, l'adresse de base est précisée par adressage absolu, alors que l'indice est précisé par adressage inhérent. À 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é.
Les deux modes d'adressage précédents sont appelés des modes d'adressage indicés, car ils gèrent automatiquement l'indice. Ils existent en deux variantes, assez similaires. La première variante ne tient pas compte de la taille de la donnée. L'adresse de base est additionnée avec l'indice, rien de plus. Le programme doit donc incrémenter/décrémenter l'indice en tenant compte de la taille de la donnée. Par exemple, pour un tableau d'entiers de 4 octets chacun, l'indice doit être incrémenté/décrémenté par pas de 4. Pour éviter ce genre de choses, la seconde variante se charge automatiquement de gérer la taille de la donnée. Le programme doit donc incrémenter/décrémenter les indices normalement, par pas de 1, l'indice est automatiquement multiplié par la taille de la donnée. Cette dernière est généralement encodée dans l'instruction, qui gère des tailles de données basiques 1, 2, 4, 8 octets, guère plus.
Pour les deux modes d'adressage précédent, l'indice est généralement mémorisé dans un registre général, éventuellement un registre entier. Mais il a existé des processeurs qui utilisaient des registres d'indice spécialisés dans les indices de tableaux. Les processeurs en question sont des processeurs assez anciens, la technique n'est plus utilisée de nos jours.
Les modes d'adressage indirect à décalage pour les enregistrements
modifierAprès avoir vu les modes d'adressage pour les tableaux, nous allons voir des modes d'adressage spécialisés dans les enregistrements, aussi appelées structures en langage C. Elles regroupent plusieurs données, généralement une petite dizaine d'entiers/flottants/adresses. Mais le processeur ne peut pas manipuler ces enregistrements : il est obligé de manipuler les données élémentaires qui le constituent une par une. Pour cela, il doit calculer leur adresse, et les modes d'adressage qui vont suivre permettent de le faire automatiquement.
Une donnée a une place prédéterminée dans un enregistrement : elle est donc a une distance fixe du début de celui-ci. En clair, l'adresse d'un élément d'un enregistrement se calcule en ajoutant une constante à l'adresse de départ de l'enregistrement. Et c'est ce que fait le mode d'adressage base + décalage. Il spécifie un registre et une constante. Le registre contient l'adresse du début de l'enregistrement, un pointeur vers l'enregistrement.
D'autres processeurs vont encore plus loin : ils sont capables de gérer des tableaux d'enregistrements ! Ce genre de prouesse est possible grâce au mode d'adressage base + indice + décalage. Il calcule l'adresse du début de la structure avec le mode d'adressage base + indice avant d'ajouter une constante pour repérer la donnée dans la structure. Et le tout, en un seul mode d'adressage.
Les modes d'adressage pseudo-absolus pour adresser plus de mémoire
modifierUne variante de l'adressage base + décalage a été utilisée sur d'anciennes architectures Motorola pour permettre d'adresser plus de mémoire. La variante en question visait à améliorer l'adressage absolu, qui intègre l'adresse à lire/écrire dans l'instruction elle-même. L'adresse en question est généralement très courte et n'encode que les bits de poids faible de l'adresse, ce qui ne permet que d'adresser une partie de la mémoire de manière absolue, là où les autres instructions ne sont pas limitées. Pour contourner ce problème, plusieurs modes d'adressages ont été inventées. Tous incorporent une adresse dans l'instruction, mais combinent cette adresse avec un registre adressé implicitement, ce qui en fait des adressages mi-indirects, mi-absolus. Nous regrouperons ces modes d'adressages sous le terme de modes d'adressages pseudo-absolus, terme de mon invention.
La première idée met les bits de poids fort de l'adresse dans un registre spécialement dédié pour. L'adresse finale est obtenue en concaténant ce registre avec l'adresse mémoire intégrée dans l’instruction. Le registre est appelé le registre de page. Un exemple est celui des premiers processeurs Motorola. Le registre de page 8 bits, l'adresse finale de 16 bits était obtenue en concaténant le registre de page avec les 8 bits fournit par adressage absolu. Le résultat est que la mémoire était découpée en blocs de 256 consécutifs, chacun pouvant servir de fenêtre de 256 octets.
Un autre exemple est celui du HP 2100, qui avait un registre de page de 5 bits et qui encodait 10 bits d'adresse dans ses instructions. Ses instructions d'accès mémoire disposaient d'un bit qui choisit quel mode d'adressage utiliser. S'il était à 0, l'adressage absolu était utilisé, le registre de page n'était pas utilisé. Mais s'il était à 1, le registre de page était utilisé pour calculer l'adresse.
Formellement, ce mode d'adressage a des ressemblances avec la commutation de banques, une technique qu'on verra dans les chapitres sur l'espace d'adressage et la mémoire virtuelle. Mais ce n'en est pas du tout : le registre de page est utilisé uniquement pour les accès mémoire avec le mode d'adressage base + décalage, mais pas pour les autres accès mémoire, qui gèrent des adresses complètes. Notons que l'usage d'un registre de page dédié fait que celui-ci est adressé implicitement.
Une évolution de l'adressage précédent est le mode page direct. Avec lui, le registre de page est étendu et contient une adresse mémoire complète. L'adresse finale n'est pas obtenue par concaténation, mais en additionnant le registre de page avec l'adresse fournie par adressage absolu. Un exemple est celui des premiers processeurs Motorola, qui géraient des adresses courtes de 8 bits. L'adresse courte de 8 bits correspondait non pas aux 256 premiers octets de la mémoire, mais à une fenêtre de 256 octets déplaçable en mémoire. La position de la fenêtre de 256 octets était spécifiée par le registre de page de 16 bits, qui précisait l'adresse du début de la fenêtre, celle de sa première donnée.
Il s'agit donc formellement d'adressage base + décalage, à un détail près : il n'y a qu'un seul registre de base. Le fait que ce registre de base soit unique fait qu'il est adressé implicitement, on n'a pas à encoder le numéro/noms de registre dans l'instruction. Le registre de base est utilisé uniquement pour l'adressage absolu, pas pour les autres accès mémoire. S'il y a des ressemblances avec la segmentation, une technique de mémoire virtuelle qu'on abordera dans quelques chapitres, ce n'en est pas vu que le registre de base est utilisé seulement pour un mode d'adressage bien précis.
Les modes d'adressage indirect mémoire
modifierLes modes d'adressage précédents mémorisent les pointeurs dans des registres, mais il existe quelques modes d'adressage qui font autrement. Ils existaient autrefois sur quelques vieux ordinateurs se débrouillaient sans registres pour les données/adresses. Avec de tels modes d'adressages, les pointeurs sont stockés en mémoire, d'où leur nom de modes d'adressage indirects mémoire.
Le plus simple d'entre eux est le mode d'adressage absolu indirect . L'instruction incorpore une adresse mémoire, mais ce n'est pas l'adresse de la donnée voulue : c'est l'adresse du pointeur qui pointe vers la donnée. Un tel mode d'adressage était utilisée sur de vieux processeurs, il était assez répandu, comparé à ses variantes. Un exemple est le cas des instructions LOAD et STORE des ordinateurs Data General Nova. Les deux isntructions existaient en deux versions, distinguées par un bit d'indirection. Si ce bit est à 0 dans l'opcode, alors l'instruction utilise le mode d'adressage absolu normal : l'adresse intégrée dans l'instruction est celle de la donnée. Mais s'il est à 1, alors l'adresse intégrée dans l'instruction est celle du pointeur.
Sur le même modèle que l’adressage indirect à registre, il a existé des modes d'adressage absolus indirects avec auto-incrément/auto-décrément. L'instruction incorpore alors l'adresse du pointeur. A chaque exécution de l'instruction, le pointeur est incrémenté ou décrémenté automatiquement. Les deux exemples les plus connus sont le PDP-8 et le Data General Nova, les autres exemples sont très rares. Sur le PDP-8, les adresses 8 à 15 avaient un comportement spécial. Quand on y accédait via adressage mémoire indirect, leur contenu était automatiquement incrémenté. Le Data General Nova avait la même chose, mais pour ses adresses 16 à 31 : les adresses 16 à 24 étaient incrémentées, celles de 25 à 31 étaient décrémentées.
Il faut noter que les modes d'adressages indicés et à décalage peuvent être rendus indirects. Par exemple, on peut imaginer un mode d'adressage indirect Base + indice. Avec lui, la somme adresse de base + indice calcule l'adresse du pointeur et non l'adresse de la donnée, ce qui rajoute un accès mémoire pour accéder à la donnée finale. Un tel mode d'adressage serait utile pour gérer des tableaux de pointeurs. En fait, tous les modes d'adressage précédents peuvent être modifiés de manière à ce que la donnée lue/écrite soit traitée comme un pointeur. Il y a donc un grand nombre de modes d'adressages indirects mémoire !
D'autres architectures supportaient des modes d'adressages indirects récursifs. l'idée était simple : le mode d'adressage identifie un mot mémoire, qui peut être soit une donnée soit un pointeur. Le pointeur peut lui aussi pointer vers une donnée ou un pointeur, qui lui-même... Une véritable chaine de pointeurs pouvait être supportée avec une seule instruction. Pour cela, chaque mot mémoire avait un bit d'indirection qui disait si son contenu était un pointeur ou une donnée. Des exemples d'ordinateurs supportant un tel mode d'adressage sont le DEC PDP-10, les IBM 1620, le Data General Nova, l'HP 2100 series, and le NAR 2. Le PDP-10 gérait même l'usage de registres d'indice à chaque étape d'accès à un pointeur.
Les modes d'adressage pour les branchements
modifierLes 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 directs
modifierAvec un branchement direct, l'opérande est simplement l'adresse de l'instruction à laquelle on souhaite reprendre.
Les branchements relatifs
modifierLes 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).
Les branchements indirects
modifierAvec 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.
Les branchements implicites
modifierLes 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.
L'instruction SKIP est équivalente à un branchement relatif dont le décalage est de 2. Il n'est pas précisé dans l'instruction, mais est implicite.
Les modes d'adressage pour les conditions/tests
modifierPour rappel, les instructions à prédicats et les branchements s’exécutent si une certaine condition est remplie. Pour rappel, on peut faire face à deux cas. Dans le premier, le branchement et l'instruction de test sont fusionnés en une seule instruction. Dans le second, la condition en question est calculée par une instruction de test séparée du branchement. Dans les deux cas, on doit préciser quelle est la condition qu'on veut vérifier. Cela peut se faire de différentes manières, mais la principale est de numéroter les différentes conditions et d'incorporer celles-ci dans l'instruction de test ou le branchement. Un second problème survient quand on a une instruction de test séparée du branchement. Le résultat de l'instruction de test est mémorisé soit dans un registre de prédicat (un registre de 1 bit qui mémorise le résultat d'une instruction de test), soit dans le registre d'état. Les instructions à prédicats et les branchements doivent alors préciser où se trouve le résultat de la condition adéquate, ce qui demande d'utiliser un mode d'adressage spécialisé.
Pour résumer peut faire face à trois possibilités :
- soit le branchement et le test sont fusionnés et l'adressage est implicite ;
- soit l'instruction de branchement doit préciser le registre à prédicat adéquat ;
- soit l'instruction de branchement doit préciser le bon bit dans le registre d'état.
L'adressage des registres à prédicats
modifierLa première possibilité est celle où les instructions de test écrivent leur résultat dans un registre à prédicat, qui est ensuite lu par le branchement. De tels processeurs ont généralement plusieurs registres à prédicats, chacun étant identifié par un nom de registre spécialisé. Les noms de registres pour les registres à prédicats sont séparés des noms des registres généraux/entiers/autres. Par exemple, on peut avoir des noms de registre à prédicats codés sur 4 bits (16 registres à prédicats), alors que les noms pour les autres registres sont codés sur 8 bits (256 registres généraux).
La distinction entre les deux se fait sur deux points : leur place dans l'instruction, et le fait que seuls certaines instructions utilisent les registres à prédicats. Typiquement, les noms de registre à prédicats sont utilisés uniquement par les instructions de test et les branchements. Ils sont utilisés comme registre de destination pour les instructions de test, et comme registre source (à lire) pour les branchements et instructions à prédicats. De plus, ils sont placés à des endroits très précis dans l'instruction, ce qui fait que le décodeur sait identifier facilement les noms de registres à prédicats des noms des autres registres.
L'adressage du registre d'état
modifierLa seconde possibilité est rencontrée sur les processeurs avec un registre d'état. Sur ces derniers, le registre d'état ne contient pas directement le résultat de la condition, mais celle-ci doit être calculée par le branchement ou l'instruction à prédicat. Et il faut alors préciser quels sont le ou les bits nécessaires pour connaitre le résultat de la condition. En conséquence, cela ne sert à rien de numéroter les bits du registre d'état comme on le ferais avec les registres à prédicats. A la place, l'instruction précise la condition à tester, que ce soit l'instruction de test ou le branchement. Et cela peut être fait de manière implicite ou explicite.
La première possibilité est d'indiquer explicitement la condition à tester dans l'instruction. Pour cela, les différentes conditions possibles sont numérotées, et ce numéro est incorporé dans l'instruction de branchement. L'instruction de branchement contient donc un opcode, une adresse de destination ou une référence vers celle-ci, puis un numéro qui indique quelle condition tester. Un exemple assez intéressant est l'ARM1, le tout premier processeur de marque ARM. Sur l'ARM1, le registre d'état est mis à jour par une opération de comparaison, qui est en fait une soustraction déguisée. L'opération de comparaison soustrait deux opérandes A et B, met à jour le registre d'état en fonction du résultat, mais n'enregistre pas ce résultat dans un registre et s'en débarrasse. Le registre d'état est un registre contenant 4 bits appelés N, Z, C et V : Z indique que le résultat de la soustraction vaut 0, N indique qu'il est négatif, C indique que le calcul a donné un débordement d'entier non-signé, et V indique qu'un débordement d'entier signé. Avec ces 4 bits, on peut obtenir 16 conditions possibles, certaines indiquant que les deux nombres sont égaux, différents, que l'un est supérieur à l'autre, inférieur, supérieur ou égal, etc. L'instruction précise laquelle de ces 16 conditions est nécessaire : l'instruction s’exécute si la condition est remplie, ne s’exécute pas sinon. Voici les 16 conditions possibles :
Code fournit par l’instruction | Test sur le registre d'état | Interprétation |
---|---|---|
0000 | Z = 1 | Les deux nombres A et B sont égaux |
0001 | Z = 0 | Les deux nombres A et B sont différents |
0010 | C = 1 | Le calcul arithmétique précédent a généré un débordement non-signé |
0011 | C = 0 | Le calcul arithmétique précédent n'a pas généré un débordement non-signé |
0100 | N = 1 | Le résultat est négatif |
0101 | N = 0 | Le résultat est positif |
0110 | V = 1 | Le calcul arithmétique précédent a généré un débordement signé |
0111 | V = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |
1000 | C = 1 et Z = 0 | A > B si A et B sont non-signés |
1001 | C = 0 ou Z = 1 | A <= B si A et B sont non-signés |
1010 | N = V | A >= B si on calcule A - B |
1011 | N != V | A < B si on calcule A - B |
1100 | Z = 0 et ( N = V ) | A > B si on calcule A - B |
1101 | Z = 1 ou ( N = 1 et V = 0 ) ou ( N = 0 et V = 1 ) | A <= B si on calcule A - B |
1110 | L'instruction s’exécute toujours (pas de prédication). | |
1111 | L'instruction ne s’exécute jamais (NOP). |
La seconde possibilité est celle de l'adressage implicite du registre d'état. C'est le cas sur les processeurs x86, où il y a plusieurs instructions de branchements, chacune calculant une condition à partir des bits du registre d'état. Le registre d'état est similaire à celui de l'ARM1 vu plus haut. Le registre d'état des CPU x86 contient 5 bits : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, d'autres une combinaison de plusieurs bits.
Instruction de branchement | Bit du registre d'état testé | Condition testée si on compare deux nombres A et B avec une instruction de test |
---|---|---|
JS (Jump if Sign) | N = 1 | Le résultat est négatif |
JNS (Jump if not Sign) | N = 0 | Le résultat est positif |
JO (Jump if Overflow) | SF = 1 ou | Le calcul arithmétique précédent a généré un débordement signé |
JNO (Jump if Not Overflow) | SF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |
JNE (Jump if Not equal) | Z = 1 | Les deux nombres A et B sont égaux |
JE (Jump if Equal) | Z = 0 | Les deux nombres A et B sont différents |
JB (Jump if below) | C = 1 | A < B, avec A et B non-signés |
JAE (Jump if Above or Equal) | C = 0 | A >= B, avec A et B non-signés |
(JBE) Jump if below or equal | C = 1 ou Z = 0 | A >= B si A et B sont non-signés |
JA (Jump if above) | C = 0 et Z = 0 | A > B si A et B sont non-signés |
JL (Jump if less) | SF != OF | si A < BA et B sont signés |
JGE (Jump if Greater or Equal) | SF = OF | si A >= BA et B sont signés |
JLE (Jump if less or equal) | SF != OF OU ZF = 1 | si A <= BA et B sont signés |
JGE (Jump if Greater) | SF = OF OU ZF = 0 | si A > B et B sont signés |