Fonctionnement d'un ordinateur/Les modes d'adressage
Une instruction n'est pas encodée n'importe comment, 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.
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 la majorité instructions ajoutent des bits 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. 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 pour les branchements.
Reste à savoir quelle est la nature de la référence : est-ce une adresse, un nombre, un nom de registre, de quoi calculer l'adresse ? Chaque manière d’interpréter la partie variable s'appellent un mode d'adressage. Un mode d'adressage indique au processeur que telle référence est une adresse, un registre, autre chose. 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).
Nous verrons dans le chapitre suivant comment sont encodées les instructions à plusieurs opérandes, ce qui dépend fortement du jeu d'instruction utilisé. Mais dans ce chapitre, nous allons nous limiter au cas où une instruction ne manipule qu'une seule opérande. De plus, nous allons nous limiter au cas où l'opérande est chargée dans un registre. La raison est que nous allons nous concentrer sur la description des modes d'adressage proprement dit. L'instruction encode donc un opcode et une référence, pas plus.
Les modes d'adressages pour les données
[modifier | modifier le wikicode]Pour 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
[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. 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)
[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 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
[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.

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
[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 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
[modifier | modifier le wikicode]Les 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
[modifier | modifier le wikicode]Les 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
[modifier | modifier le wikicode]Les 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
[modifier | modifier le wikicode]Le 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
[modifier | modifier le wikicode]Aprè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 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 directs
[modifier | modifier le wikicode]Avec un branchement direct, l'opérande est simplement l'adresse de l'instruction à laquelle on souhaite reprendre.

Il s'agit d'une sorte d'équivalent à l'adressage immédiat/absolu, mais pour les branchements.
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).

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.

Il s'agit d'une sorte d'équivalent à l'adressage indirect à registre, mais pour les branchements.
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.
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
[modifier | modifier le wikicode]Pour 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
[modifier | modifier le wikicode]La 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
[modifier | modifier le wikicode]La 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 |
Les modes d'adressage obsolètes : données et pointeurs
[modifier | modifier le wikicode]Dans cette section, nous allons voir quelques modes d'adressage autrefois utilisés sur les ordinateurs historiques, d'avant les années 90. Ils ne sont plus utilisés aujourd'hui, aucun processeur ne les supporte. Cependant, ils reviendront plus tard dans ce cours, aussi je préfère en parler maintenant. De plus, certains ont un lien avec ce qui a été dit précédemment. Nous allons tout d'abord voir un mode d'adressage pour les pointeurs.
Les modes d'adressage indirect mémoire
[modifier | modifier le wikicode]Les modes d'adressage pour les pointeurs mémorisent les pointeurs dans des registres, mais il existe quelques modes d'adressage qui mémorisent les pointeurs en mémoire RAM, à une adresse bien précise. Avec de tels modes d'adressages, le processeur accède à une adresse mémoire pour récupérer le pointeur, et l'utiliser pour un second accès. L'accès est donc indirect, par l'intermédiaire du pointeur, d'où leur nom de modes d'adressage indirects mémoire. Ils étaient utilisés autrefois sur quelques vieux ordinateurs se débrouillaient sans registres pour les données/adresses.
Du moment qu'un mode d'adressage fournit une adresse mémoire, il peut être rendu indirect. Par exemple, on peut imaginer un mode d'adressage indirect Base + indice : la somme base + indice calcule l'adresse du pointeur et non l'adresse de la donnée. Un tel mode d'adressage serait utile pour gérer des tableaux de pointeurs. 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 !
Le plus simple d'entre eux est le mode d'adressage absolu indirect. L'instruction incorpore une adresse mémoire, comme dans l'adressage absolu. Sauf qu'il s'agit d'un adressage indirect : l'adresse n'est pas l'adresse de la donnée voulue, mais l'adresse du pointeur qui pointe vers la donnée. Un exemple est le cas des instructions LOAD et STORE des ordinateurs Data General Nova. Les deux instructions 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.
Il a existé des modes d'adressage absolus indirects avec auto-incrément/auto-décrément, où le pointeur est incrémenté ou décrémenté automatiquement lors de l'exécution de l'instruction. 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.
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.
Une curiosité historique : l'instruction Index next de l'Apollo Guidance Computer
[modifier | modifier le wikicode]Les tout premiers ordinateurs ne supportaient aucun mode d’adressage indirect. L'utilisation de tableaux ou de structures de données était un véritable calvaire, qui se résolvait à grand coup de code automodifiant. Les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination été incorporée dans l'instruction via adressage absolu, mais était changée via code automodifiant. Et quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant, voire pour s'en passer.
Un exemple est celui de l'instruction Index next instruction, que nous appellerons INI, qui a été utilisée sur des architectures comme l'Apollo Guidance Computer et quelques autres. Elle additionne une certaine valeur à l'instruction suivante. Par exemple, si l’instruction suivante est une instruction LOAD adresse 50, l'INI permet d'y ajouter la valeur 5, ce qui donne LOAD adresse 55.
Elle est utilisée pour émuler un adressage absolu indicé : on utilise l'INI pour ajouter l'indice à l'instruction LOAD suivante. La valeur à ajouter est précisée via mode d'adressage absolu est lue depuis la mémoire. Un point important est que l'addition a lieu à l'intérieur du processeur, pas en mémoire RAM/ROM. Le mode d'adressage ne fait pas de code auto-modifiant, l'instruction modifiée reste la même qu'avant en mémoire RAM. Elle est altérée une fois chargée par le processeur, avant son exécution.
Notons que cette instruction est aussi utilisée pour modifier des branchements : si l'instruction suivante est l'instruction JUMP à adresse 100, on peut la transformer en JUMP à adresse 150. Elle peut en théorie changer l'opcode d'une instruction, ce qui permet en théorie de faire des calculs différents suivant le résultat d'une condition. Mais ces cas d'utilisation étaient assez rares, ils étaient peu fréquents.
Les modes d'adressage pseudo-absolus pour adresser plus de mémoire
[modifier | modifier le wikicode]Une 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.
L'adressage relatif pour les données
[modifier | modifier le wikicode]L'adressage relatif est utilisé pour les branchements, pour calculer l'adresse de destination. Mais il peut en théorie être utilisé pour les données. En clair, l'adresse d'une donnée est calculée en ajoutant au program counter un décalage, un offset. Il s'agit d'une variante de l'adressage base + décalage, sauf que l'adresse de base est le program counter. Il était très utilisé sur les anciens ordinateurs, qui encodaient leurs instructions sur un faible nombre de bits et ne pouvaient pas encoder d'adresses complètes.
Un exemple est celui du PDP-8, encore lui. Il avait des instructions de 12 bits, et les adresses mémoire faisaient la même taille. L'opcode était codé sur 3 bits, l'instruction incorporait une adresse codée sur 7 bits, il restait deux bits pour le mode d’adressage. Vous remarquerez que les adresses font 12 bits, mais que les instructions incorporent seulement les 7 bits de poids faible. Il faut donc trouver les 5 bits de poids fort manquants. Pour cela, trois modes d'adressage sont possibles
- avec l'adressage absolu, les 5 bits de poids fort sont mis à 0 ;
- avec l'adressage PC-relatif, les 5 bits de poids fort étaient les 5 bits de poids fort du program counter ;
- avec l'adresse indirect mémoire, l'adresse 7 bit poijnte vers un pointeur en mémoire, qui fait 12 bits.
Les deux bits du mode d'adressage permettent d'indiquer quelle option est choisie. Le premier bit indiquait si le mode d'adressage utilisé était le mode d'adressage indirect mémoire ou non. Il était à 1 pour le mode d'adressage indirect mémoire, à 0 sinon. Le second bit indiquait s'il fallait utiliser l'adressage absolu (0) ou relatif au program counter.
Opcode | Mode d'adressage | Adresse mémoire | |
---|---|---|---|
Opcode | Bit d'indirection | Bit d'adressage absolu/relatif | Adresse mémoire |
3 bits | 1 bit | 1 bit | 7 bits |