Fonctionnement d'un ordinateur/Les architectures à capacités

Un livre de Wikilivres.

Les architectures à capacité (capability based processors) sont des jeu d'instruction qui font un usage un peu particulier de la segmentation. Sur la segmentation usuelle, les segments sont de grande taille et il y en a assez peu. Sur les architectures à capacité, les segments sont petits et nombreux. De plus, ils sont protégés par des mécanismes de protection mémoire très poussés, qui font que les architectures à capacité se démarquent du reste.

Pour comprendre ce qu'elles font, nous devons faire quelques rappels sur les pointeurs. Les pointeurs sont des variables dont le contenu est une adresse mémoire, qui permettent de localiser un morceau de données. Dans les grandes lignes, ils servent dès que l'on manipule des 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. En temps normal, les pointeurs sont juste des adresses mémoires sans rien de plus. Mais sur les architectures à capacité, toutes les adresses sont combinées avec des droits d'accès. Elles forment alors des capacités, des adresses protégées.

Un point important est que les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à la donnée tant qu'ils disposent de la capacité associée. En clair : tous les programmes sont dans le même espace d'adressage ! Et on pourrait croire que les capacités sont des adresses physiques, mais non ! Les capacités sont des adresses virtuelles, ce qui fait que les données associées à un pointeur peuvent être swappées sur le disque dur. Les architectures à capacité gérent parfaitement la mémoire virtuelle !

Tout programme a accès à un certain ensemble de capacités, qu'il ne peut pas modifier. Il ne peut pas générer de capacité par lui-même, seul le système d'exploitation peut forger une capacité. Cette forge se fait lors de l'allocation mémoire. Pour rappel, sur tous les OS récents, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Il recoit alors une portion de mémoire à laquelle il est seul à accéder. Sur un ordinateur normal, le système d'exploitation retourne alors un pointeur, qui contient la première adresse de la portion de mémoire allouée. Le pointeur peut alors être modifié par le programme receveur, il peut l'altérer, et en faire ce qu'il veut. Sur une architecture à capacité, le système d'exploitation forge une capacité qu'il est seul à pouvoir modifier. La forge d'une capacité se fait avec une instruction du processeur, qui n'est accessible qu'en espace noyau, ce qui fait que seul le noyau de l'OS peut l'exécuter.

Les capacités : les principes de haut-niveau[modifier | modifier le wikicode]

La majorité des architectures à capacités sont des processeurs qui disposent d'une forme améliorée de segmentation. Sur les architectures à capacité, chaque segment est associé à une capacité, un pointeur amélioré qui permet d'identifier un segment et de l'utiliser. Elles regroupent plusieurs informations : l'adresse de base du segment, sa taille, et surtout ses autorisations d'accès. Les capacités sont équivalentes aux descripteurs de segments de la segmentation.

Si les capacités peuvent être utilisées par les programmes, seul le système d'exploitation peut forger ou modifier les capacités, notamment en modifier les droits d'accès. Les architectures à capacités ne gèrent donc que des pointeurs protégés, là où les architectures sans capacités peuvent manipuler des pointeurs à loisir.

Un espace d'adressage unique : un partage de données facilité[modifier | modifier le wikicode]

Avec la segmentation normale, chaque programme a son propre espace d'adressage et sa propre table des segments. Par contre, une architecture à capacité utilise un espace d'adressage unique pour tous les programmes en cours d’exécution. Tous les programmes partagent le même espace d'adressage. Une conséquence est que le partage de données est facilité.

Avec la segmentation normale, il y a deux moyens pour partager des données. La première demande de partager un segment entre deux applications, ce qui demande de configurer correctement les deux tables des segments des deux processus. L'autre solution consiste à faire en sorte que deux segments se recouvrent, ce qui est possible car une adresse physique peut correspondre à plusieurs couples (segment-offset). Avec une architecture à capacité, des programmes différents peuvent utiliser une même capacité, pour pointer vers le même objet. L'espace d'adressage partagé fait que la capacité est un couple segment-offset utilisable par les deux applications, sans avoir à manipuler les tables des segments.

La liste de capacités[modifier | modifier le wikicode]

Sur ces architectures, il faut distinguer une capacité des autres formes de données/adresses/autres. Pour cela, on dispose de deux techniques, complémentaires.

La première est celle des pointeurs taggués. Chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une architecture à tags, ou tagged architectures. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.

La seconde méthode est de regrouper les capacités d'un programme dans une zone de mémoire dédiée, protégée en écriture. Tout programme a accès à une liste de capacités, appelée la C-list, généralement placée en mémoire RAM. La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est innaccesible en écriture par le programme, seul le noyau du système d'exploitation en est capable. Pour le programme en espace utilisateur, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. S'il a besoin de plus de mémoire, le programme peut demander au système d'exploitation la création d'une nouvelle capacité, ce qui est équivalent à faire une demande d'allocation mémoire.

Pour faire une comparaison, la table des capacités est équivalente à la table des segments, sauf qu'elle est accessible en lecture par le programme exécuté. Le programme peut lire la table des segments et charger un segment dans un registre de relocation. Un descripteur de segment est donc une capacité, dans les grandes lignes. La C-list doit être protégée en écriture. La solution la plus utilisée consiste à regrouper la C-list dans un segment dédié. Le processeur gère donc deux types de segments : les segments de capacité pour les C-list, les segments de données pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.

La gestion de la table des segments est relativement similaire. Pour rappel, la table des segments mémorise toutes les informations essentielles pour manipuler les segments. Elle contient plusieurs descripteurs de segment, chacun associé à un segment. Ces derniers mémorisent l'adresse de base du segment, sa taille et ses droits d'accès. Chaque programme dispose de sa propre table des segments, qui liste tous les segments accessibles par le programme. La manipulation de la table des segments d'un programme est le fait du système d'exploitation, les tables de segment étant placées dans des zones mémoire accessibles seulement en espace noyau.

Les registres de capacité[modifier | modifier le wikicode]

Les architectures à capacité ont des registres séparés pour les entiers et les capacités. La raison principale est une question de sécurité, mais une raison plus pragmatique est que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités sont identiques aux registres relocation, voire aux registres dédiés aux segments.

Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans la capacité associée au segment, la seconde information est fournie par l'instruction d'accès mémoire. Un accès mémoire précise donc deux choses : dans quel registre de capacité se trouve la capacité voulue, la position de la donnée dans le segment. La position dans le segment peut être dans un registre, ce qui ressemble à du mode d'adressage (Base + Index), ou être codée en adressage immédiat, ce qui donne un mode d'adressage équivalent à du (Base + Offset).

Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre C-list et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects.

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'Intel iAPX 432[modifier | modifier le wikicode]

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 en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude. Autre détail : ce processeur ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1. Il avait été conçu pour exécuter en grande partie le langage ADA, un langa assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.

Ce processeur utilise la segmentation. Sur l'Intel iAPX432, chaque segment (et donc chaque objet) pouvait mesurer jusqu'à 64 kibioctets. Le processeur pouvait gérer jusqu'à 2^24 segments différents. Chaque segment est découpé en deux parties de tailles égales : une partie contenant les données de l'objet, et une partie pour les capacités. Ces capacités pouvaient pointer vers d'autres segments (pour créer des structures de données assez complexes), par exemple. Au fait : les capacités étaient appelées des Access Descriptors dans la documentation officielle.

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 432[modifier | modifier le wikicode]

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 432[modifier | modifier le wikicode]

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.
Encodage des instructions de l'Intel iAPX-432.

Conclusion[modifier | modifier le wikicode]

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) :

Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :