Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87
Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des extensions x86.
Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l'extension x87. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE.
Sur les tout premiers processeurs x86, le support du x87 n'était pas implémenté dans le processeur. Il était délégué à des coprocesseurs, appelés des coprocesseurs x87. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Par la suite, les processeurs x86 ont intégré une FPU qui exécutait les instructions x87 nativement. Les coprocesseurs sont devenus inutiles. Nous verrons en détail ces coprocesseurs dans le prochain chapitre. Pour le moment, nous allons juste parler du jeu d'instruction x87.
Les registres x87
[modifier | modifier le wikicode]L'extension x87 fournit, en plus des instructions, plusieurs registres :
- 8 registres pour les opérandes flottantes ;
- 3 registres d'état pour configurer les exceptions, les arrondis, etc ;
- 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès.
Une organisation en pseudo-pile
[modifier | modifier le wikicode]Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus.
Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.
Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur.
En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.
Le registre d'état
[modifier | modifier le wikicode]Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé Tag Word. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre Tag Word indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc.
- Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
- Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
- Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
- Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.
Le processeur x87 contient aussi deux registres d'état, nommés Control Word et Status Word. Le registre Status Word contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc.
| Bit | Utilité |
|---|---|
| TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |
| U | Détecte les underflows : est mis à 1 en cas d'underflows. |
| O | Détecte les overflows : est mis à 1 en cas d'overflows. |
| Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |
| D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |
| I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |
Le registre Control Word fait 16 bits et configure la gestion des arrondis.
| Bit | Utilité |
|---|---|
| Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |
| Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé
|
| Precision Control | `Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754.
|
Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage.
Les instructions flottantes x87
[modifier | modifier le wikicode]Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire.
Les instructions de calcul x87
[modifier | modifier le wikicode]On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste :
- l'instruction FCOS pour le cosinus ;
- l'instruction FSIN pour le sinus ;
- l'instruction FPTAN pour la tangente ;
- l'instruction FPATAN pour l'arc tangente ;
- des instructions de calcul de logarithmes ou d'exponentielles.
La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 :
- FCOM : compare le contenu du registre 0 avec une constante flottante ;
- FCOMI : compare le contenu des registres 0 et 1 ;
- FICOM : compare le contenu du registre 0 avec une constante entière ;
- FTST : compare le registre numéroté 0 avec la valeur 0.
Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS).
Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables.
Les instructions mémoire x87
[modifier | modifier le wikicode]Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire.
| Instruction | Description |
|---|---|
| FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |
| FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |
| FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |
D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres.
Le phénomène de double arrondi
[modifier | modifier le wikicode]Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes.
Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc.
Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits.
Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. Pire que ça, le résultat du calcul dépend de l'ordre dans lequel on fait les instructions. Les arrondis seront différents selon que l'on fait une écriture à tel moment plutôt qu'un autre. Il s'agit du phénomène dit de 'double arrondi (double rounding).

Sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée.

