« Fonctionnement d'un ordinateur/Les jeux d'instructions » : différence entre les versions

aucun résumé des modifications
Aucun résumé des modifications
Les instructions d'un processeur dépendent fortement du processeur utilisé. La liste de toutetoutes les instructions qu'un processeur peut exécuter s'appelle son '''jeu d'instructions'''. Celui-ci définit les instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les anciens macintoshs (la génération de macintosh produits entre 1994 et 2006) utilisaient un jeu d'instruction différent : le PowerPC (depuis 2006, les macintoshs utilisent un processeur X86). Mais les architectures x86 et Power PC ne sont pas les seules au monde : il existe d'autres types d'architectures qui sont très utilisées dans le monde de l’informatique embarquée et dans tout ce qui est tablettes et téléphones portables derniers cris. On peut citer notamment les architectures ARM, MIPS et SPARC. Pour résumer, il existe différents jeux d'instructions, que l'on peut classer suivant divers critères.
 
==L'adressage des opérandes==
|}
 
Les architectures à registres sont subdivisées en sous-catégories : les architectures LOAD-STORE, les architectures à accimulateursaccumulateurs et les architectures à registre générales. Pour résumer, nous allons parler des architectures suivantes : les machines à pile, les machines à file, les architectures à accumulateur, les architectures mémoire-mémoire et les architectures à registres.
 
===Les architectures mémoire-mémoire===
Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre que l'accumulateur. Toutes les opérandes hors-accumulateur étaient lues en mémoire. Sur ces processeurs, les modes d'adressages supportés étaient les modes d'adressages implicite, absolus, et immédiat. Ces architectures sont parfois appelées '''architectures 1-adresse''', pour une raison simple : la majorité des instructions manipulent deux opérandes, ce qui fait qu'elles devaient lire une opérande depuis la RAM. Pour ces opérations, le résultat ainsi qu'une des opérandes sont stockés dans l'accumulateur, et adressés de façon implicite, seule la seconde opérande étant adressée directement.
 
Avec ces seuls modes d'adressages, l'utilisation de tableaux ou de structures était un véritable calvaire. Pour améliorer la situation, les processeurs à accumulateurs ont alors incorporés des '''registres d'Index''', pour faciliter les calculs d'adresse mémoire. Ils étaient capables de stocker des indices de tableaux, ou des constantes permettant de localiser une donnée dans une structure. Au départ, ces processeurs n'utilisaient qu'un seul registre d'Index, accessible et modifiable via des instructions spécialisées, qui se comportait comme un second accumulateur spécialisé dans les calculs d'adresses mémoire. Les modes d'adressages autorisés restaient les mêmes qu'avec une architecture à accumulateur normale. La seule différence, c'est que le processeur contenait de nouvelles instruction capables de lire ou d'écrire une donnée dans/depuis l'accumulateur, qui utilisaient ce registre d'Index de façon implicite. Mais avec le temps, nos processeurs finirent par incorporer plusieurs de ces registres. Nos instructions de lecture ou d'écriture devaient alors préciser quel registre d'Index utiliser. Le mode d'adressage ''Indexed Absolute'' vit le jour. Les autres modes d'adressages, comme le mode d'adressage Base + Index ou indirects à registres étaient plutôt rares à l'époque et étaient difficiles à mettre en œuvre sur ce genre de machines.
 
Ensuite, ces architectures s’améliorèrent un petit peu : on leur ajouta des registres capables de stocker des données. L’accumulateur n'était plus seul au monde. Mais attention : ces registres ne peuvent servir que d’opérande dans une instruction, et le résultat d'une instruction ira obligatoirement dans l'accumulateur. Ces architectures supportaient donc le mode d'adressage inhérent.
Les '''processeurs à registres''' peuvent stocker temporairement des données dans des registres généraux ou spécialisés. Pour échanger des données entre la mémoire et les registres, on peut utiliser une instruction à tout faire : MOV. Sur d'autres, on utilise des instructions séparées pour copier une donnée de la mémoire vers un registre (LOAD), copier le contenu d'un registre dans un autre, copier le contenu d'un registre dans la mémoire RAM (STORE), etc.
 
Ces architectures à registres généraux (ainsi que les architectures ''Load-Store'' qu'on verra juste après) sont elles-même divisées en deux sous-classes bien distinctes : les architectures 2 adresses et les architectures 3 adresses. Cette distinction entre architecture 2 et 3 adresses permet de distinguer les modes d'adressages des opérations arithmétiques manipulant deux données : additions, multiplications, soustraction, division, etc. Ces instructions ont donc besoin d'adresser deux données différentes, et de stocker le résultat quelque part. Il leur faut donc préciser trois opérandes dans le résultat : la localisation des deux données à manipuler, et l'endroit où ranger le résultat.
* Sur les '''architectures à deux adresses''', le résultat d'une instruction est stocké à l'endroit indiqué par la référence du premier opérande : cette donnée sera remplacée par le résultat de l'instruction. Avec cette organisation, les instructions ne précisent que deux opérandes. Mais la gestion des instructions est moins souple, vu qu'un opérande est écrasé. Avec cette organisation, les instructions sont plus courtes.
* Sur les '''architectures à trois adresses''', on peut préciser le registre de destination du résultat. Ce genre d'architectures permet une meilleure utilisation des registres, mais les instructions deviennent plus longues que sur les architectures à deux adresses.
[[File:Is0addr.png|vignette|upright=0.75|Adressage des opérandes sur une machine à pile.]]
 
Sur certaines machines FIFO/LIFO, la pile et la file sont localisées non pas dans le processeur, mais dans la mémoire RAM. Ces machines ont besoin d'un registre pour stocker l'adresse du sommet de la pile/file. Dans le cas d'une machine à pile, celui-ci est appelée le ''stack pointer'', ou '''pointeur de pile'''. Sur les machines à file, il est appelé le ''queue pointer'', ou encore le '''pointeur de file'''. Les instructions de calcul utilisent uniquement les registres de pile pour savoir où se trouvent les opérandes. En clair, toutes les instructions utilisent uniquement le mode d'adressage implicite. Il va de soit que pour simplifier la conception du processeur, celui-ci peut contenir des registres internes pour stocker temporairement les opérandes des calculs, mais ces registres ne sont pas accessibles au programmeur : ce ne sont pas des registres architecturaux. Par exemple, sur certaines machines à pile, tout ou partie de la pile est stockée directement dans des registres internes au processeur, pour gagner en performance.
 
====Les instructions des machines à pile====
 
Ces jeux d’instructions ont des instructions pour ajouter des opérandes dans la FIFO/LIFO ou en retirer. Dans ce qui va suivre, nous allons prendre l'exemple d'une machine à pile, mais il existe des équivalents pour les machines à file. Dans les grandes lignes, il y a deux instructions principales nommées PUSH et POP pour l'ajout et le retrait d'opérandes dans la pile, ainsi que quelques instructions annexes sur certaines machines. Les machines à file possèdent des instructions équivalentes, à savoir deux instructions ''Queue'' et ''Unqueue'' pour ajouter ou retirer une opérande dans la file, ainsi que quelques instructions annexes. Voici les deux instructions principales de gestion de la pile, présentes sur toutes les architectures à pile :
* L'instruction PUSH permet d'empiler une donnée. Elle prend l'adresse de la donnée à empiler, charge la donnée, et met à jour le stackpointeur pointerde pile.
* L'instruction POP dépile la donnée au sommet de la pile, la stocke à l'adresse indiquée dans l'instruction, et met à jour le stackpointeur pointerde pile.
 
[[File:Instruction Push.png|centre|vignette|upright=2.5|Instruction Push.]]
[[File:Instruction Swap.png|centre|vignette|upright=2|Instruction Swap.]]
 
Enfin, on trouve des instructions pour sauvegarder la totalité de la pile ou de la file. Ces instructions servent surtout pour les appels de fonctions/sous-programmes, chacun d'entre eux demandant de vider complètement les registres du processeur, ainsi que la file ou la pile. Ils sont l'équivalent sur ces architectures des instructions de spilling, qui sauvegardent la totalité des registres du processeur en mémoire RAM, pour les récupérer à la fin de l'éxecutionl’exécution de la fonction.
 
====Les modes d'adressage des machines à pile/file====
====Avantages et désavantages====
 
Sur ces architectures, les programmes utilisent peu de mémoire. La raison à cela est que les instructions sont très petites : on n'a pas besoin d'utiliser de bits pour indiquer la localisation des données dans la mémoire, sauf pour Pop et Push. Vu que les programmes créés pour ces machines sont souvent très petits, on dit que la code density (la densité du code ('code density'') est bonne. Par contre, une bonne partie des instructions de notre programmes seront des instructions Pop et Push qui ne servent qu'à déplacer les opérandes entre la RAM et la FIFO/LIFO. Une bonne partie des instructions ne sert donc qu'à manipuler la mémoire et pas à faire des calculs. Sans compter que notre programme comprendra beaucoup d'instructions comparé aux autres types de processeurs. Enfin, ces machines n'ont pas besoin d'utiliser beaucoup de registres pour stocker leur état : un ''Stack Pointer'' et un ''Program Counter'' suffisent. Les machines à pile furent les premières à être inventées et utilisées : dans les débuts de l’informatique, la mémoire était rare et chère, et l'économiser était important. Ces machines à pile permettaient d'économiser de la mémoire facilement et d'utiliser peu de registres, ce qui était le compromis idéal pour l'époque.
 
Les architectures à pile et à file ont plusieurs défauts. En premier lieu, la complexité de la gestion de la pile/file entraineentraîne l'usage d'un grand nombre d'instructions d'accès mémoire. Mais surtout, il est impossible de réutiliser une donnée chargée dans la pile, toute opération dépilant ses opérandes. Cela entraineentraîne un grand nombre d'accès en mémoire RAM, défaut rédhibitoire quand la RAM est lente et peu chère et où la hiérarchie mémoire dicte sa loi. Les autres classes d'architectures n'ont pas ces défauts et font usage de modes d'adressage autres que le mode d'adressage implicite. L'usage de ces modes d'adressages permet d'éviter d'avoir à copier des données dans une pile, les empiler, et les déplacer avant de les manipuler. Le nombre d'accès à la mémoire est donc plus faible comparé à une machine à pile pure.
 
==Les jeux d'instruction RISC vs CISC==
Les jeux d'instructions CISC sont les plus anciens et étaient à la mode jusqu'à la fin des années 1980. A cette époque, on programmait rarement avec des langages de haut niveau et beaucoup de programmeurs codaient en assembleur. Avoir un jeu d'instruction complexe, avec des instructions de "haut niveau" qu'on ne devait pas refaire à partir d'instructions plus simples, facilitait la vie des programmeurs.
 
Cette complexité des jeux d'instructions n'a pas que des avantages "humains", mais a aussi quelques avantages techniques. Le premier est une meilleure densité de code : un programme codé sur CISC utilise moins d'instructions et prend moins de mémoire. A l'époque des processeurs CISC, la mémoire était rare et chère, ce qui faisait que les ordinateurs n'avaient pas plusieurs gigaoctets de mémoire : économiser celle-ci était crucial. Cet avantage était donc crucial, ce qui contrebalançait les défauts de ces architectures. Ces défauts étaient essentiellement le fait que le grand nombre d'instructions entraineentraîne une grande consommation de transistors et d'énergie. La difficulté de conception de ces processeur était aussi sans précédent.
 
===Les processeurs RISC===
De plus, de nos jours, les différences entre CISC et RISC commencent à s'estomper. Les processeurs actuels sont de plus en plus difficiles à ranger dans des catégories précises. Les processeurs actuels sont conçus d'une façon plus pragmatique : au lieu de respecter à la lettre les principes du RISC et du CISC, on préfère intégrer les techniques et instructions qui fonctionnent, peu importe qu'elles viennent de processeurs purement RISC ou CISC. Les anciens processeurs RISC se sont ainsi garnis d'instructions et techniques de plus en plus complexes et les processeurs CISC ont intégré des techniques provenant des processeurs RISC (pipeline, etc). La guerre RISC ou CISC n'a plus vraiment de sens de nos jours.
 
En parallèle de ces architectures CISC et RISC, qui sont en quelques sorte la base de tous les jeux d'instructions, d'autres classes de jeux d'instructions sont apparus, assez différents des jeux d’instructions RISC et CISC. On peut par exemple citer le ''Very Long Instruction Word'', qui sera abordé dans les chapitres à la fin du tutoriel. La plupart de ces jeux d'instructions sont implantés dans des processeurs spécialisés, qu'on fabrique pour une utilisation particulière. Ce peut être pour un langage de programmation particulier, pour des applications destinées à un marche de niche comme les supercalculateurs, etc.
 
==Les jeux d'instructions spécialisés==
===Les architectures parallèles===
 
Certaines architectures sont conçues pour pouvoir exécuter plusieurs instructions en même temps, lors du même cycle d’horloge : elles visent le traitement de plusieurs instructions en parallèle, d'où leur nom d’'''architectures parallèles'''. Ces architectures visent la performance, et sont relativement généralistes, à quelques exceptions près. On peut, par exemple, citer les architectures ''very long instruction word'', les architectures dataflow, les processeurs EDGE, et bien d'autres. D'autres instructions visent à exécuter une même instruction sur plusieurs données différentes : ce sont les instructions SIMD, vectorielles, et autres architectures utilisées sur les cartes graphiques actuelles. Nous verrons ces architectures plus tard dans ce tutoriel, dans les derniers chapitres.
 
===Les ''Digital Signal Processors''===
 
Certains jeux d'instructions sont dédiés à des types de programmes bien spécifiques, et sont peu adaptés pour des programmes généralistes. Parmi ces jeux d'instructions spécialisés, on peut citer les fameux jeux d'instructions '''Digital Signal Processor''', aussi appelés des DSP. Nous reviendrons plus tard sur ces processeurs dans le cours, un chapitre complet leur étant dédié, ce qui fait que la description qui va suivre sera quelque peu succintesuccincte. Ces DSP sont des processeurs chargés de faire des calculs sur de la vidéo, du son, ou tout autre signal. Dès que vous avez besoin de traiter du son ou de la vidéo, vous avez un DSP quelque part, que ce soit une carte son ou une platine DVD.
 
Ls DSP ont un jeu d'instruction similaire aux jeux d'instructions RISC, avec quelques instructions supplémentaires spécialisées pour faire du traitement de signal. On peut par exemple citer l'instruction phare de ces DSP, l'instruction MAD, qui multiplie deux nombres et additionne un 3éme au résultat de la multiplication. De nombreux algorithmes de traitement du signal (filtres FIR, transformées de Fourier) utilisent massivement cette opération. Ces DSP possèdent aussi des instructions dédiées aux boucles, ou des instructions capables de traiter plusieurs données en parallèle (en même temps). De plus, les DSP utilisent souvent des nombres flottants assez particuliers qui n'ont rien à voir avec les nombres flottants que l'on a vu dans le premier chapitre. Certains DSP supportent des instructions capables d'effectuer plusieurs accès mémoire en un seul cycle d'horloge : ils sont reliés à plusieurs bus mémoire et sont donc capables de lire et/ou d'écrire plusieurs données simultanément. L'architecture mémoire de ces DSP est une architecture Harvard, couplée à une mémoire multi-ports. Les caches sont rares dans ces architectures, quoique parfois présents.
En regardant dans les langages de programmation un peu plus connus, on peut aussi citer des processeurs spécialisés pour Java. Certains processeurs ARM, qu'on trouve dans des système embarqués, sont de ce type. Mais ceux-ci sont un petit peu à part et ne sont pas vraiment des architectures dédiées. En réalité, ces processeurs sont une implémentation matérielle de la machine virtuelle Java. Rappelons que la machine virtuelle JAVA est un design de processeur comme un autre, avec un jeu d'instruction simple, une architecture à pile, etc. Le code machine associé à cette architecture est appelé le ''bytecode'' Java. En temps normal, le ''bytecode'' Java n'est pas exécuté directement par le processeur, qui utilise un langage machine différent. A la place, le ''bytecode'' est traduit dans le langage machine adéquat, par un interpréteur/compilateur. c'est ce qui permet au ''bytecode'' Java d'être portable sur des architectures différentes. En fait, le ''bytecode'' Java est utilisé comme intermédiaire : on compile du code Java en ''bytecode'' Java, qui est traduit sur l'architecture cible quand on en a besoin. Les architectures Java permettent de se passer de l'étape de traduction ''bytecode'' -> langage machine, vu que eur langage machine est le ''bytecode'' Java lui-même.
 
===Les architectures à capabilitéscapacités===
 
D'autres processeurs incorporent des méthodes qui permettent d’améliorer la suretésûreté de fonctionnement ou la sécurité. Elles permettent d'éviter certaines attaques logicielles, comme des virus ou des accès non autorisés directement en matériel. Ces jeux d'instructions sont conçus en synergie avec certains systèmes d'exploitation. Sur les '''architectures à capacités''', chaque adresse utilisée par un morceau de code se voit attribuer des autorisations d'accès, qui peuvent varier. Par exemple, le code d'un système d'exploitation aura accès en écriture à certaines adresses critiques, qui contiennent des paramètres de sécurité critique, mais les autres programmes n'y auront accès qu'en lecture (voire pas du tout). Les droits d'accès ne seront pas les mêmes, et les capacités différentes (parfois pour une même adresse). Ces droits d'accès sont rassemblés dans une suite de bits, que l'on appelle une ''capability'', ou capacité. Celles-ci décrivent :
 
* quels sont les droits d'accès en lecture et écriture : lecture seule, pas de lecture ou écriture possible, et ainsi de suite ;
* et sur certaines architectures, quel est le type de la donnée.
 
Les instructions de lecture et écriture prennent comme argument une adresse et une capacité. Pour avoir accès à une adresse, le programme doit fournir automatiquement la capacité qui va avec : il doit la charger dans des registres spécialisés. Ces capacités sont mémorisées dans la mémoire RAM : chaque programme ou fonction a accès à une liste de capacités en mémoire RAM. Les instructions qui manipulent les registres de capacités ne peuvent pas, par construction, augmenter les droits d'accès d'une capacité : ils peuvent retirer des droits, pas en ajouter. Ce mécanisme interdit donc à tout sous-programme ou programme de modifier une adresse qui n'est pas dans sa liste de capacité : le programme ne pourra pas récupérer la capacité, et n'aura pas accès à l'adresse voulue. Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de suretésûreté de fonctionnement indéniable. Du moins, c'est la théorie : tout repose sur l'intégrité des listes de capabilitycapacités : si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
 
Un chapitre entier sera dédié à ces architectures, à la fin du livre.