Fonctionnement d'un ordinateur/Les architectures à capacités
Certains processeurs incorporent des méthodes qui permettent d’améliorer la sureté 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. Il s'agit des capability based processors, ou architectures à capacités. Dans les grandes lignes, il s'agit d'architectures dont le langage machine ressemble beaucoup à un langage orienté objet.
Les capacités : les principes de haut-niveauModifier
Sur les architectures à capacités, chaque donnée manipulée par le processeur est stockée dans un objet, une sorte de conteneur générique placé quelque part dans la mémoire, sans que l'on ait de moyen de savoir où. Cet objet peut être absolument n'importe quoi : cela peut être un objet spécifié par le programmeur, ou des objets prédéfinis lors de la fabrication du processeur. Par exemple, on peut considérer chaque périphérique comme un objet, auquel on a défini des méthodes bien particulières qui permettront de communiquer avec celui-ci ou de le commander. Sur d'autres architectures, chaque programme en cours d’exécution est considéré comme un objet, avec des méthodes permettant d'agir sur son état. On peut ainsi stopper l’exécution d'un programme via des méthodes adaptées, par exemple. Mais ce qui va nous permettre d'adapter des langages de programmation orientés objet sur de telles architectures, c'est la possibilité de créer soi-même des objets non définis lors de la fabrication du processeur.
Un autre point est que la notion même d'adresses mémoire n'existe pas (ou presque). Au lieu d’utiliser des adresses mémoire et autres mécanismes divers et variés, chaque objet se voit attribuer un identifiant unique (deux objets ne peuvent avoir le même identifiant), qui ne change pas au cours du temps. L'identifiant est défini lors de la création de l'objet et ne peut pas réutilisé pour un autre objet (sauf une fois que l'objet possédant cet identifiant est détruit). Dans les grandes lignes, on peut voir cet identifiant comme une sorte d'adresse virtuelle, qui permet de localiser l'objet mais peut correspondre à une adresse physique totalement différente. De plus, chaque objet est associée à des autorisations d'accès. Par exemple, le code d'un système d'exploitation aura accès en écriture à certains objets 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). L'identifiant et les droits d'accès sont rassemblés dans que l'on appelle une capacité. Elles décrivent les droits d'accès, l'identifiant et éventuellement le type de la donnée. Le type peut correspondre à des types prédéfinis, mais peut correspondre à des types définis par le programmeur.
Les instructions de lecture et écriture prennent comme argument un identifiant et une capacité. Pour avoir accès à un identifiant, le programme doit fournir automatiquement la capacité qui va avec et la charger dans des registres spécialisés. Les capacités sont mémorisées dans la mémoire RAM et sont regroupés au même endroit. Dans le détail, 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 un objet qui n'est pas dans sa liste de capacité. Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
L'implémentation matérielle des capacitésModifier
Divers mécanismes dépendants du processeur permettent d'implémenter l'héritage ou d'autres fonctionnalités objet en autorisant des manipulations, accès, copies ou partages temporaires de listes de capacités. Généralement, ce genre de fonctionnalité objet est géré directement au niveau des instructions du processeur : le processeur contient pour ce faire des instructions spéciales. Ces instructions sont souvent des instructions d'appels de fonction particulières. Pour ceux qui ne le savent pas, une instruction d'appel de fonction sert à demander au processeur d’exécuter une fonction bien précise. Sur les processeurs optimisés pour les langages procéduraux, une fonction est identifiée par son adresse, tandis que ces processeurs fournissent sa capacité à l'instruction chargée d’exécuter notre fonction. Sur ces processeurs, de nombreuses instructions d'appel de fonction sont disponibles : par exemple, l'instruction pour appeler une fonction définie dans la classe de l'objet qu'elle va manipuler ne sera pas la même de celle devant appeler une fonction héritée d'une autre classe : il faudra en effet faire quelques accès pour modifier ou accéder à des listes de capacités extérieures, etc.
Accéder aux données d'un objet demande de connaitre son adresse mémoire. Pour cela, il faut convertir l'identifiant d'objet en une adresse mémoire. Cette conversion s’effectuera dans la MMU, mais la méthode de conversion dépend de la conception du processeur, aussi il sera difficile de faire des généralités sur le sujet. Toujours est-il que le processeur doit contenir une liste de correspondance entre objet et adresse de celui-ci. On peut préciser que ces techniques s'appuient souvent sur la segmentation. Chaque objet est stocké dans un segment, qui commence à une adresse physique bien précise. Les attributs de l'objet sont stockés dans ce segment, à une place prédéfinie. L'identifiant est alors simplement l'adresse virtuelle du segment qui contient l'objet. Il existe cependant une différence entre l'usage de la segmentation usuelle et la segmentation pour l'implémentation des capacités. Dans le premier cas, chaque programme utilise son propre espace d'adressage et a sa propre table des segments. Par contre, un OS à capacité va utiliser une table des segments identique pour tous les programmes en cours d’exécution. Ce faisant, plusieurs programmes différents peuvent avoir accès à un même segment/objet, si leurs droits d'accès le permettent.
Premier exemple : le processeur RekursivModifier
Nous allons commencer par aborder le processeur Rekursiv. Ne soyez pas perturbé par son nom : il ne s'agit pas d'une coïncidence, comme on le verra plus tard. Ce processeur fut inventé par la compagnie Linn Product, un fabricant de matériel Hi-Fi, qui voulait améliorer ses chaînes de production automatisées. Celles-ci fonctionnaient avec un ordinateur DEC VAX assez correct pour l'époque. Cette compagnie avait lancé un grand projet de rajeunissement de sa chaîne de production. Au tout début, le projet consistait simplement à créer de nouveaux outils logiciels pour faciliter le fonctionnement de la chaîne de production. Au cours de ce projet, un langage de programmation orienté objet, le Lingo, fut créé dans ce but. Mais les programmes créés dans ce langage fonctionnaient vraiment lentement sur les DEC VAX de l'entreprise. L'entreprise, qui n'avait pas hésité à créer un nouveau langage de programmation pour ce projet, prit ce problème de performances à bras-le-corps et décida carrément d'inventer un nouveau processeur spécialement adapté à Lingo. Ainsi naquit le processeur Rekursiv, premier processeur orienté objet de son genre. Rekursiv était au départ prévu pour être utilisé sur des stations de travail Sun 3. Mais malgré ses nombreuses qualités, Rekursiv ne s'est pas beaucoup vendu dans le monde : à peine 20 exemplaires furent vendus. La majorité des acheteurs étaient des chercheurs en architecture des ordinateurs, et rares furent les entreprises qui achetèrent des processeurs Rekursiv. Il faut dire que ce processeur était relativement spécialisé et difficile à utiliser, sans compter que d'autres processeurs concurrents firent leur apparition, comme l'Intel 432. Ce processeur fut donc un échec commercial retentissant, malgré une réussite technique indéniable.
Vu de loin, ce processeur ressemble à un processeur tout à fait normal, découpé en quatre grands circuits principaux bien connus :
- Numerik : l'unité de calcul ;
- Logik : le séquenceur ;
- Objekt : une MMU orientée objet ;
- et Klock, une unité regroupant des timers et un générateur d'horloge.
Le support du paradigme objet était géré par Logik et par Objekt, aussi nous verrons plus en détail leurs possibilités dans la suite de ce tutoriel. Mais nous n'allons pas passer sous silence Numerik et Klock. Klock est chargée de synchroniser les différents composants de ce processeur. Plus précisément, elle contient des timers, des composants permettant de mesurer des durées, et de quoi générer le signal d'horloge du processeur. Ce processeur avait une fréquence d'environ 10 Mhz, ce qui n'était pas si mal pour l'époque.
Numerik est le nom donné à l'ALU de ce processeur. Son jeu d'instructions est donc assez limité. On peut néanmoins dire que cette unité de calcul contient un circuit capable d'effectuer des multiplications ainsi qu'un barrel shifter, un circuit capable d'effectuer des instructions de décalage et de rotation. Cette unité de calcul est rattachée à 16 registres 32 bits, rassemblés dans un register file. Numerik est capable de manipuler des nombres de 32 bits. Cette unité de calcul est un peu particulière : elle est formée de petites unités de calcul 4 bits, de marque AMD : des AMD2900, pour être précis. Ces unités de calculs AMD de 4 bits sont reliées entre elles pour former Numerik. Cette technique qui consiste à créer des unités de calcul plus grosses à partir d’unités de calcul plus élémentaires s'appelle en jargon technique du Bit Slicing.
Son séquenceur était micro-codé, son Control Store de Rekursiv n'étant autre qu'une mémoire d'environ 64 kibioctets. Elle était accessible en écriture, ce qui fait qu'il était parfaitement possible de reprogrammer le jeu d'instructions du processeur sans restrictions. Cela pouvait même se faire à l’exécution, ce qui pouvait servir à adapter le jeu d'instructions à un langage particulier, voire l'adapter temporairement à un objet que l'on était en train de manipuler. L'ensemble des instructions du processeur (son jeu d'instructions) était donc programmable ! Ce processeur possédait même une petite particularité : on pouvait, de par l'organisation de son micro-code, créer des instructions récursives ! Ainsi, les instructions de copie ou de recherche dans un arbre ou une liste présentes dans son jeu d'instructions étaient codées via ce genre d'instructions récursives.
Certaines des instructions du processeur étaient adaptées au paradigme objet et permettaient de gérer plus simplement les diverses fonctionnalités orientées objet au niveau du matériel. Ces instructions étaient toutes micro-codées, bien évidemment. Par exemple, il existait des instructions CREATE, chacune capable de créer un objet d'une certaine classe. Cette instruction n'était autre qu'un constructeur pour un certain type d'objet. Elle avait besoin de certaines données pour fonctionner (sa taille, son type et les valeurs initiales de ses attributs) qui étaient passés par la pile vue ci-dessus. Il existait aussi des instructions de transfert de messages entre objets (comme SEND), des instructions pour accéder à un champ d'un objet localisé sur la pile (GET), pour modifier un champ dans l'objet (PUT), et bien pire encore. On peut signaler que ces instructions ne pouvaient opérer que sur certains types d'objets : certaines instructions ne pouvaient ainsi manipuler que des objets d'une certaine classe et pas d'une autre.
Mais ce qui fait que Rekursiv était un processeur orienté objet ne vient pas seulement de son jeu d'instructions : le principal intérêt de Rekursiv tient dans son unité chargée de gérer la mémoire. Les capacités de ce processeur étaient codées sur 40 bits. Objekt, la MMU, se chargeait de convertir les capacités en adresses mémoires de façon transparente pour les instructions. La MMU stockait diverses informations sur chaque objet : ainsi, la MMU pouvait retrouver l'adresse mémoire de l'objet, son type, et sa taille à partir de l'identifiant. Ces informations étaient stockées dans la mémoire, dans une table segments dédiée. Du fait de l'usage de la segmentation, un objet gardait en permanence la même capacité. On pouvait déplacer l'objet dans la mémoire, son identifiant restait le même (alors que son adresse mémoire changeait). De même, la MMU pouvait décider de déplacer un objet sur le disque dur sans que le programme ne s'en aperçoive.
Objekt implémentait un Garbage Collector matériel assez simple, mais suffisamment efficace. Pour rappel, un garbage collector, ou ramasse-miettes, est un programme ou un système matériel qui se charge de supprimer de la mémoire les objets ou données dont on n'a plus besoin. Dans certains langages de programmation comme le C ou le C++ , on est obligé de libérer la mémoire à la main. Ce n'est pas le cas pour certains langages orientés objet, comme JAVA ou Lingo : un garbage collector permet de gérer la mémoire automatiquement, sans demander au programmeur de se fatiguer à le faire lui-même (du moins, en théorie). Ce garbage collector avait souvent besoin de déplacer des objets pour faire un peu de place en mémoire, et compacter les objets ensemble, pour faire de la place. Le fait que les objets soient manipulés avec une capacité facilitait énormément le travail du garbage collector matériel.
Second exemple : l'Intel iAPX 432Modifier
Passons maintenant à un autre processeur orienté objet un peu plus connu : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu. Il faut dire que ce processeur avait des performances assez désastreuses et des défauts techniques certains. Par exemple, ce processeur ne contenait pas de mémoire cache et n'avait pas de registres (c'était une machine à pile). Autre détail : ce processeur ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1 (une sombre histoire de mode d'adressage immédiat non supporté). De plus, celui-ci avait été conçu pour exécuter en grande partie le langage ADA, un langage de programmation très utilisé dans le domaine de l'embarqué. Malheureusement, le compilateur ADA pour ce processeur était mauvais et donnait des programmes aux performances assez mauvaises. Sans compter que l'ADA n'était pas très populaire chez les programmeurs et n'était utilisé que par les militaires et les entreprises travaillant sur des systèmes embarqués ou critiques, ce qui n'a pas aidé à faire vendre ce processeur.
Le modèle mémoire de l'Intel iAPX 432Modifier
Ce processeur utilise la segmentation pour définir ses objets. Chaque objet est stocké dans un segment avec toutes ses données et autres informations permettant de déterminer son état. Chaque segment est découpé en deux parties de tailles égales : une partie contenant les données de l'objet, et une partie pour des informations supplémentaires. Ces informations pouvaient être des capacités pointant vers d'autres objets (pour créer des objets assez complexes), par exemple. Au fait : les capacités étaient appelées des Access Descriptors dans la documentation officielle.
Sur l'Intel iAPX432, chaque segment (et donc chaque objet) pouvait mesurer jusqu'à 64 kibioctets : c'est très peu, mais suffisant pour stocker des objets suffisamment gros pour que cela ne pose pas vraiment de problèmes. Ce processeur pouvait gérer jusqu'à 2^24 segments différents.
L'Intel 432 possédait dans ses circuits un garbage collector matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
Le support de l'orienté objet sur l'Intel iAPX 432Modifier
L'Intel 432 est conçu pour permettre l'utilisation de « types », de classes de base, déjà implémentées dans le processeur, mais cela ne suffit évidemment pas à supporter la programmation orientée objet. Pour cela, le processeur permet de définir ses propres classes, utilisables au besoin, et définies par le programmeur.
L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des domains objects, qui contiennent un ensemble de capacités pointant chacune vers des fonctions. Chacune de ces fonctions peut accéder à un nombre restreint d'objets, tous du même type, et à rien d'autre. Chaque domain object est divisé en deux parties : une partie publique, qui contient des capability identifiant les fonctions exécutables au besoin par tout morceau de code ayant accès au domain object, et une partie privée, qui contient des capability identifiant des fonctions/méthodes internes au domain object : seules les fonctions déclarées dans la partie publique possèdent des capability pointant vers ces fonctions et donc, seules ces fonctions de la partie publique peuvent les utiliser. En clair, ces domains objects ne sont rien de moins que l'ensemble des méthodes d'une classe ! Chacun de ces domain objects possédait une capacité rien que pour lui, qui permettait de l'identifier et que l'on devait utiliser pour accéder aux fonctions qu'il contient. Évidemment, ce processeur supportait de nombreuses instructions et fonctionnalités permettant à des capacités pointant vers des fonctions publiques d’être présentes dans des domains objects différents. Celles-ci pouvaient être paramétrées de façon plus ou moins fine afin de choisir quelles fonctions d'un domain object devaient être partagées ou non. Cela permettait de supporter des fonctionnalités objet telles que l'héritage, l'inheritance, etc.
Sur ce processeur, chaque processus est considéré comme un objet à part entière. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est défini par un objet, stocké en mémoire, qu'il est possible de manipuler : toute manipulation de cet objet permettra d'effectuer une action particulière sur notre processeur. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un objet, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
Le jeu d'instruction de l'Intel iAPX 432Modifier
Ce processeur est capable d’exécuter pas moins de 230 instructions différentes. Le processeur supporte certains objets de base, prédéfinis dans le processeur. Il fournit des instructions spécialement dédiées à la manipulation de ces objets, et contient notamment des instructions d'appel de fonction assez élaborées. Il contient aussi des instructions n'ayant rien à voir avec nos objets, qui permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc. Beaucoup de ces instructions sont micro-codées. Le processeur est une machine à pile.
Ce processeur contenait aussi des instructions spécialement dédiés à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales. Ces instructions chargées de prendre en charge le travail d'un système d'exploitation étaient des manipulations comme un changement de contexte ou un passage de message entre processus et se contentaient de faire des manipulations sur des objets représentant le processeur, des processus, ou d'autres choses dans le genre.
On peut aussi préciser que ces instructions sont de longueur variable. Sur un processeur normal, les instructions ont une longueur qui est souvent multiple d'un octet, mais notre Intel iAPX 432 fait exception à cette règle : ses instructions peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Sur l'Intel iAPX 432, les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière. Comme vous allez le voir dans un instant, l'encodage des instructions reflète directement l'organisation de la mémoire en segments : le jeu d'instructions a dû s'adapter à l'organisation de la mémoire.
- Le premier champ s'appelle classe. Il permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
- Le second champ, le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
- Le troisième champ, reference, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capability de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
- Le dernier champ s'appelle l'opcode et permet d’identifier l'instruction à effectuer : s'agit-il d'une addition, d'une création d'objet, d'un passage de message entre deux processus, d'une copie d'un objet sur la pile, etc.
ConclusionModifier
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
Pour avoir plus d’informations sur le jeu d'instructions du processeur Rekursiv, voyez le lien suivant :
Voici un docuement qui décrit le fonctionnement de l'Intel iAPX432 :
Et enfin, voici un document à propos du processeur HISC :