l'assembleur  [livre #4061]

par  pierre maurette



L'architecture
IA

Le but de ce chapitre n'est pas de traiter d'histoire industrielle. Notre intention est d'aborder, d'une manière qui se veut la plus agréable possible, un certain nombre de spécificités de l'architecture PC, parfois ardues.

A l'origine de la lignée se trouve l'IBM PC doté d'un 8088 cadencé à 4,77 MHz et d'un système d'exploitation MS-DOS ou PC DOS. A la fréquence de l'horloge près, cette première machine est encore présente dans nos compatibles actuels, et semble devoir y rester pendant encore quelques années. Cette présence n'est d'ailleurs pas sans poser des problèmes au programmeur, mais le programmeur aime les problèmes.

Il sera donc utile de connaître avec une bonne précision l'architecture de cette machine historique, avant de nous pencher sur les PC actuels. La programmation sous DOS, donc sur un modèle 8086, existe encore, il suffit pour s'en convaincre de parcourir les groupes de discussions traitant de l'assembleur. Les choix des universités, é tatsUniennes et autres, et des organismes de formation y sont pour beaucoup.

Nous avons utilisé le mot compatible. Dans le monde PC sous Windows, la compatibilité peut se situer à plusieurs niveaux liés, mais fondamentalement différents.

  Le microprocesseur  : nous venons de voir qu'un Pentium 4 ou un Athlon XP, comme il y a quelques années un 80386 ou un K6, encapsulent en quelque sorte l'antique 8086, permettant de simuler parfaitement son fonctionnement en mode réel. En mode virtuel 8086, plusieurs peuvent être émulés simultanément. C'est le domaine de l'architecture x86, appelée IA-32  par Intel, et où AMD joue un rôle de plus en plus important.
Pour respecter cette étonnante compatibilité ascendante, chaque étape ne peut que rajouter des possibilités, comme un jeu de registres et les instructions associées, dont l'utilisation n'a rien d'obligatoire. Quand c'est la taille des registres généraux qui augmente, l'ancien jeu de registres demeure à l'intérieur des nouveaux, et l'ancien code doit continuer à s’exécuter.
Nous assistons aujourd'hui à l'arrivée d'une nouvelle génération 64 bits, qui va encapsuler la technologie 32 bits, elle-même conservant sa propre encapsulation des modes 16 bits. La compatibilité 8086 n'est pas une obligation éternelle, et constitue un frein aux performances. Néanmoins, l'heure de la disparition n'a pas encore sonné. Voir à ce sujet, en fin de chapitre, la présentation de l'architecture AMD64/Hammer.

  L'ordinateur  : celui que vous achetez aujourd'hui est honorablement compatible avec celui de 1980, compatibilité ascendante s'entend. Manquent malgré tout certains accessoires, comme un Basic en ROM ou un lecteur de cassettes. Le format des disquettes a changé, la capacité du disque dur peut poser problème. Il devient courant de proposer des solutions 100% USB, donc sans interfaces série ni parallèle, le lecteur de disquette étant remplacé par la connexion réseau/internet. Malheureusement, cette solution est plus viable sur un ordinateur de bureau que sur un modèle nomade.
La norme de fait compatible PC est évolutive et ouverte. Cette norme n'a pas de véritable tuteur ; elle dépend des décisions à la fois des fondeurs de microprocesseurs, des développeurs de systèmes d'exploitation et des fabricants de cartes mère et de composants. Elle intéresse le programmeur en assembleur au plus haut point, au niveau des pilotes de périphériques par exemple.
Les circuits coupleurs d'E/S de l'IBM PC original ne sont présents qu'au travers de circuits à haute intégration. Vous ne trouverez pas physiquement, sur les cartes actuelles, de coupleur 8250 ou 8255. Vous pourrez en revanche les programmer comme s'ils y étaient.
Derrière cette compatibilité matérielle se trouve souvent la notion de couches logicielles, à niveau d'abstraction croissant au fur et à mesure que l'on s'éloigne du matériel. C'est ainsi qu'un programme peut adresser l'imprimante connectée sur un port USB2, même s'il a été écrit antérieurement à l'apparition de cette norme. Le langage assembleur peut bénéficier de cette abstraction, mais dans sa spécificité s'adresse plus aux couches basses.

  Le système d'exploitation  : sous Windows XP, certaines applications anciennes ne fonctionneront pas. Et l'évolution de la lignée Windows ne va rien arranger dans ce domaine. Par exemple, le mode MS-DOS n'est désormais plus qu'une simple émulation. C'est le système d'exploitation qui va interdire aux applications normales la programmation directe des coupleurs d'E/S, par ailleurs, et c'est heureux, possible pour le système lui-même.
Il y a très grossièrement trois périodes bien distinctes : la période DOS, dont fait partie Windows jusqu'à 3.11, puis les Windows 9X, et enfin la technologie 2000/XP. Nous devons citer ici une solution alternative aux divers Windows, Linux, bien qu'elle ne soit pas traitée spécifiquement ni en tant que support ni en tant que cible dans cet ouvrage.

Ces trois domaines sont étroitement imbriqués. Au départ du fil de compatibilité, se trouve une définition de machine, l'IBM PC, mélange des caractéristiques du 8088, du PC lui-même et du système d'exploitation MS-DOS (ou PC DOS). Un composant important est le BIOS, mémoire ROM de démarrage et interface logiciel bas niveau/matériel. Grâce à lui, par exemple, un clavier connecté sur une interface USB sera reconnu par un système d'exploitation antérieur à la naissance de cette interface.

Une machine actuelle bootera sur une disquette MS-DOS (à partir d'un numéro de version malgré tout), ou sur un DOS compatible comme DR-DOS. Cela signifie que, non seulement le microprocesseur sera configuré en compatibilité, mais également la carte graphique, le clavier, etc.

Nous avons évoqué la compatibilité ascendante du microprocesseur ; il existe également une compatibilité horizontale entre les constructeurs, typiquement aujourd’hui Intel et AMD, qui permet à l'un de se rendre compatible avec les améliorations initiées par l'autre, en utilisant parfois des technologies très différentes. En matière de microprocesseurs, le clonage est interdit, sauf accord particulier. Mais faire un microprocesseur qui respecte toutes les caractéristiques du modèle semble autorisé. Disons qu’AMD a commencé par faire des puces qui ressemblaient furieusement à ceux d’Intel, pour aujourd’hui proposer une production originale. Nous sommes loin de la fabrication sous licence Intel ou Motorola de circuits compatibles, par des fabricants japonais ou européens.

Une évolution du microprocesseur apportant des améliorations significatives, comme les propositions 64 bits d'AMD et d'Intel, ont besoin d'une carte mère pour exister, mais surtout d'un système d'exploitation adapté pour prendre en charge ces nouvelles caractéristiques. L'idéal est que cette nouvelle plate-forme admette également les anciens systèmes d'exploitation. Pour gagner l’adhésion des constructeurs ou assembleurs de micro-ordinateurs, le support de Microsoft et de la communauté Linux est indispensable. C'est le cas de la technologie AMD64.

Nous allons maintenant, dans la continuité du chapitre précédent, étudier le matériel du modèle initial IBM PC/8086. Nous en terminons ainsi avec le hardware. La segmentation de la mémoire sera un gros morceau présenté à cette occasion. Seront évoquées les autres caractéristiques de l'architecture, registres, flags, interruptions ; mais, étant peu différentes de l'architecture actuelle, elles seront détaillées plus tard.

Ensuite, l'évolution depuis ce premier modèle jusqu'aux produits actuels sera survolée, pour en arriver à l'architecture actuelle IA-32.

Nous terminerons enfin le chapitre par un aperçu sur les architectures 64 bits.

2.1 Le 8086/8088

Si le premier IBM PC embarquait un 8088 à 4,77 MHz, c'est bien le 8086 qui est à l'origine de toute l'histoire. Le 8088 fut développé ultérieurement, peut-être dans le but précisément de motoriser plus économiquement ce premier ordinateur personnel d'IBM.

Le 8088 est strictement un 8086, donc un microprocesseur 16 bits. Simplement, son bus externe de données était réduit à 8 bits ; un transfert sur 16 bits nécessitait deux accès mémoire. Cette particularité est absolument transparente pour le programmeur. La seule infime différence au niveau du cœur est la longueur du tampon de préchargement, qui passe de 6 dans le 8086 à 4 dans le 8088. Mais répétons que le code à écrire est strictement le même pour les deux modèles.

Il semble bien qu'un 8088 fut aussi cher à produire qu'un 8086, voire un peu plus. L'économie devait donc se réaliser sur la mémoire. 9 boîtiers monobits suffisaient, au lieu du double pour un 8086. Il est courant, dans le domaine du microprocesseur, de vendre moins cher qu'un autre un produit plus cher à produire, et ayant demandé des études supplémentaires.

Remarque

La détection d'erreur par parité

Quand nous avons évoqué les 9 boîtiers, donc 9 bits, il ne s’agissait pas d’une coquille. Au moment de l'écriture mémoire d'un mot de 8 bits, un circuit indépendant calculait le 9 ème , pour que le nombre total de bits à 1 soit impair. Cette propriété était vérifiée à la lecture par un circuit qui pouvait générer une interruption en cas d'erreur. Cette technique utilisée sur les compatibles PC jusqu'au début des années 90, qui ne concernait pas le programmeur et ne ralentissait pas la machine, explique la présence sur le marché de barrettes mémoires 9 bits.

La gamme 80186 est un peu à part. Ces processeurs possèdent le jeu d'instructions du 8086 légèrement amélioré, et intègrent un certain nombre de circuits périphériques. Le 80186 dans ses versions les plus complètes, ressemble à un microcontrôleur plus qu'à un microprocesseur.

Des microprocesseurs compatibles ont très rapidement vu le jour. Comme plus tard AMD, le japonais NEC proposait les modèles V20 et V30 compatibles 8088 et 8086, et légèrement plus performants.

2.1.1 Le hardware

Le 8086 et le 8088 se présentaient sous la forme d'un même boîtier DIL 40. Le brochage est pratiquement identique :

Brochage des 8086 et 8088
figure 2.01 Brochage des 8086 et 8088 [the .swf]

La barre au-dessus du nom d'un signal indique simplement que celui-ci est actif à l'état bas. Nous négligeons cette barre dans le texte, pour des raisons typographiques.

Le 8086/8088 pouvait être positionné dans un mode parmi deux : minimum ou maximum. Ce choix est fait de façon statique, lors de la conception de la carte accueillant le circuit.

Le mode maximum permet par exemple l'utilisation conjointe de plusieurs processeurs, en délégant une partie du contrôle de bus à un circuit spécialisé de type 8288. C'est le signal en entrée MN/MX qui sélectionne le mode. L'IBM PC pouvait accueillir un coprocesseur arithmétique 8087 optionnel ; il était dessiné en mode maximum et intégrait un 8288.

Certains signaux qui ne sont plus disponibles au niveau du 8088 en mode maximum se retrouvent sur le 8288. Leur rôle reste le même, et nos commentaires également.

Il a déjà été signalé que le 8086/8088 possède deux instructions spécifiques (IN et OUT) et une patte spécialisée, pour accéder à une zone E/S de 64 Ko, indépendante de la mémoire principale.

Le bus est multiplexé dans le temps, c'est-à-dire qu'il est bus d'adresses puis de données, à des moments différents. Un accès mémoire ou I/O, en lecture ou en écriture, demande donc une succession d'actions relativement complexe au niveau des bus : présentation et verrouillage d'une adresse, présentation de la donnée à écrire, ou acquisition de la donnée à lire, fin des opérations. La documentation divise cette chronologie en quatre phases, nommées T1, T2, T3 et T4. Pour résoudre les problèmes liés par exemple à des mémoires à temps d'accès trop long, un certain nombres de cycles TW, ou Wait States, vont parfois s'insérer entre T3 et T4. Dans l'IBM PC, le signal WAIT en provenance de la logique de gestion des accès transite par le circuit d'horloge de type 8284A, qui fabrique les signaux CLK, RESET et READY.

Attention

Quel que soit le modèle, la mémoire est accessible octet par octet

Il est tout à fait possible de lire (écrire) en mémoire un mot de 16 bits à une adresse impaire, même sur le 8086. En d’autres termes, l’alignement de la mémoire sur le mot n’est pas nécessaire. L’instruction : mov word ptr[adresse impaire], valeur est parfaitement valable. Elle demandera simplement deux accès mémoire, au lieu d’un pour un accès aligné sur un 8086. Voir à ce sujet le signal BHE, patte 34 dans le tableau. L’alignement des grands tableaux de données est donc un point majeur d’optimisation. Le mauvais alignement est plus ou moins pénalisant pour tous les processeurs de la famille, sauf bien entendu le 8088.

Ce point n'a pas changé depuis, et sur un 686 ou un ATLON 64, toute adresse à l'octet près est valable pour toute donnée en mémoire, même si elle n'est pas toujours souhaitable.

Dans les tableaux suivants, nous voyons de façon simplifiée les signaux importants entrant et sortant dans le 8088, en prenant souvent en exemple le premier IBM PC. Le premier tableau concerne les signaux présents dans les deux modes de fonctionnement, le second les signaux spécifiques au mode minimum, et enfin le troisième ceux spécifiques au mode maximum. Rappelons à propos du second tableau que beaucoup de signaux y figurant seront disponible également en mode maximum, car refabriqués par ailleurs, par un 8288, dans le cas de l'IBM PC.

 

Les signaux du 8086/8088 présents dans les deux modes

Nom

Pattes

Sens

Description

GND

1,20

E

GrouND, masse. Référence 0 volt de l'alimentation.

Vcc

40

E

+5 volts de l'alimentation.

CLK

19

E

CLocK : horloge de base à 4,77 MHz.  C'est la période de cette horloge qui correspond au cycle.

AD0 à AD15

2 à 16, 39

E/S

Représentent pendant T1 les 16 premiers bits du bus d'adresses, et pendant T2, T3, TW et T4, les 16 bits du bus de données. Pour le 8088, remplacer par AD0 à AD7 et A8 à A15, le bus de données étant réduit à 8 bits.

A16/S3
A17/S4
A18/S5
A19/S6

35 à 38

S

Pendant T1, complètent le bus d'adresses à 20 bits par A16..A19.
Pendant T2, T3, TW et T4 sont, soit à l'état bas, soit S3..S6 fournissent des informations, comme l'état du masque d'interruption ou le type d'accès mémoire, code, donnée, pile ou autre.

BHE/S7

34

S

Bus High Enable/Status : gère la lecture ou l'écriture d'un seul octet à la fois, ou d'un mot de 16 bit. Le résultat est le suivant, en fonction du couple de valeurs BHE,A0 :

0,1 : 1 octet haut vers/depuis adresse impaire.

1,1 : Rien.

SSO

34

S

System Status Output : uniquement sur le 8088 et en mode minimum, différencie la lecture de code de celle de données.

RD

32

S

Read : ce signal indique que le cycle en cours (T2, T3 et TW) est une lecture mémoire ou E/S.

MN/MX

33

E

MiNimum/MaXimum : positionne le processeur en mode maximum à 0 (GND), en mode minimum à 1 (Vcc).

READY

22

E

Indique au microprocesseur que la mémoire ou le périphérique en cours d’accès a terminé son transfert. Élaboré (synchronisé) à partir de WAIT par le circuit d'horloge 8284A dans l'IBM PC.

INTR

18

E

INTerrupt Request : demande d'interruption externe masquable. C'est le niveau sur cette patte qui est lu, en fin de chaque instruction effectuée par le microprocesseur.

NMI

17

E

Non Maskable Interrupt. C'est un front sur cette entrée qui envoie le processeur dans une interruption non masquable.

TEST

23

E

Cette patte est testée par l'instruction WAIT, et détermine si le processeur continue (patte à 0) ou devient effectivement inactif.

RESET

21

E

Ce signal, à l'état bas au repos, doit passer pendant au moins 4 cycles par l'état haut. C'est quand il redescend que le microprocesseur entame son cycle de (re)démarrage.

 

Les signaux du 8086/8088 spécifiques au mode minimum

Nom

Pattes

Sens

Description

HOLD
HLDA

30, 31

E/S

L'entrée HOLD transmet au microprocesseur une demande de prise de possession du bus. Celui-ci répond sur la sortie HLDA (HoLD Acknowledge), tout en s'isolant des différents bus, y compris de contrôle, en les mettant dans le troisième état, haute impédance. Le dialogue inverse a lieu quand le demandeur redescend le HOLD.

M/IO

28

S

Ce signal différencie un accès mémoire d'un accès E/S. En rapport direct avec les instructions assembleur IN et OUT.

WR

29

S

WRite : indique que le cycle en cours (T2, T3 et TW) est une écriture mémoire ou E/S.

INTA

24

S

INTerrupt Acknowledge : Acquittement d'interruption. Il s'agit d'un signal de lecture équivalent à RD, pour le traitement des interruptions.

ALE

25

S

Address Latch Enable : un front qui indique l'instant où l'adresse présente sur les 20 bits du bus est valable et peut être verrouillée.

DT/R

27

S

Data Transmit/Receive : différencie cycle de lecture et cycle d'écriture. Utile pour les amplis de bus bidirectionnels.

DEN

26

S

Data Enable : indique la validité des données sur le bus. Utile pour les amplis de bus bidirectionnels.

SSO

34

S

System Status Output : uniquement sur le 8088 ; différencie la lecture de code de celle de données.

 

Les signaux du 8086/8088 spécifiques au mode maximum

Nom

Pattes

Sens

Description

S0, S1, S2

26, 27, 28

S

3 bits à destination du 8288 qui, une fois décodés par celui-ci, indiquent quel cycle effectue le 8086/8088, et permettent au 8288 de fabriquer tous les signaux utiles de contrôle d'accès au bus.

RQ/GT0
RQ/GT1

30, 31

E/S

ReQuest/GrantT (demande /autorisation). C'est par ces deux fils que le processeur peut négocier la possession du bus avec d'autres propriétaires possibles de celui-ci.

LOCK

29

S

Par cette sortie, le processeur signale son refus temporaire de partager l'accès au bus. Elle intéresse le programmeur, puisque c'est lui qui va décider de ce verrouillage, en préfixant telle ou telle instruction par LOCK. Utile pour traiter les sémaphores. Toujours d'actualité.

QS0
QS1

24, 25

S

Queue Status : les combinaisons de ces deux bits indiquent l'état de la queue de préchargement d'instructions du processeur. Utilisé par exemple par le 8087 pour se synchroniser.

Dans le chronogramme général de lecture et écriture suivant, nous voyons les signaux les plus importants. Il n'est pas indispensable de maîtriser ce type de document, mais il peut constituer une aide pour la lecture de ce qui précède et de ce qui suit.

Chronologie de base d'accès mémoire et E/S
figure 2.02 Chronologie de base d'accès mémoire et E/S [the .swf]

Les lignes ADRESSE/STATUS et ADRESSE/DATA représentent les deux parties du bus multiplexé, respectivement la partie supérieure (adresse et bits d'état) et la partie supérieure (adresse et données).

Notons que les signaux RD et INTA ont strictement la même fonction et le même comportement.

Rien de ce qui précède n'est absolument indispensable pour le programmeur. Pour dresser un parallèle avec la conduite d'une automobile, celle-ci est possible sans rien connaître du piston ni du vilebrequin. Ignorer le rôle de l'embrayage est plus délicat.

À partir de ce qui suit, nous entrons dans le domaine du très utile et de l'indispensable.

2.1.2 Structure interne

Voici une représentation, symbolique, de la structure interne du 8086, qui fait suite aux représentations plus générales vues au chapitre précédent :

Structure interne du 8086
figure 2.03 Structure interne du 8086 [the .swf]

Cette représentation, qui n'est qu'une esquisse, est valable également pour le 8088, à deux différences près : la queue d'instructions sera réduite à 4 octets, et le bus de données sera sorti en largeur 8 bits vers l'extérieur. C'est ce détail qui justifie notre aparté précédent au sujet du prix de revient du 8088 : il semble bien qu'il y ait un travail de multiplexage supplémentaire pour le 8088, par rapport au 8086.

Nous constatons immédiatement la structuration en deux blocs, une unité de traitement  EU  déjà en grande partie connue, et une unité d'échanges et d'approvisionnement, BIU .

 

Au centre de l'EU, l'ALU, unité arithmétique et logique. Elle paraît très vascularisée, puisqu'en contact avec tous les autres sous-ensembles, par l'intermédiaire ou non d'un bus 16 bits local. Les registres généraux accessibles au programmeur en font également partie. Nous voyons qu'il s'agit de registres 16 bits, pouvant pour certains être abordés comme deux registres 8 bits.

Le cœur de l'unité est le bloc de logique de contrôle. En fonction de l'instruction en cours de traitement, il va positionner l'ALU pour une opération particulière, une addition par exemple. Il va également programmer la logique interne pour qu'à l'entrée de l'ALU, dans deux registres tampons, se trouvent les valeurs à traiter. Quand nous aurons étudié le jeu d'instructions, nous verrons qu'un au moins de ces tampons va chercher son contenu dans les registres généraux. L'autre sera alimenté, soit par un registre général, soit via le BIU, par une valeur immédiate ou lue en mémoire.

Une valeur immédiate est de celles qui sont fournies explicitement par le programmeur ; elle est donc présente dans le flux du code, en tant qu'opérande mélangé aux instructions. Quand les valeurs ne sont pas immédiates, c'est leurs adresse qui sont, sous une forme ou une autre, dans ce flux.

Le traitement d'une instruction n'a rien d'immédiat. Certaines vont demander jusqu'à quelques dizaines de cycles. Parmi ces longues instructions, la multiplication et la division. L'instruction AAM (ASCII Adjust for Multiplication), qui permet, comme nous le verrons, d'effectuer des multiplications sur des entiers codés en BCD, demande 83 cycles d'horloge. Elles se comportent, dans l'ALU et le bloc logique, comme de véritables sous-programmes, à base de micro-instructions. Nous sommes avec ces instructions au cœur de l'architecture CISC.

La BIU gère tout ce qui est échanges avec la mémoire, et les entrées/sorties qui sont une forme particulière de mémoire. Cela concerne donc en premier lieu l'élaboration de l'adresse à déposer sur le bus au temps T1.

La mémoire segmentée

Ce qui précède est pour le programmeur du niveau du culturel, plus ou moins important. Ce qui sui doit absolument être maîtrisé pour la programmation assembleur.

Nous voyons que le microprocesseur fabrique une adresse sur 20 bits, qui permettent d'adresser 1048576 octets, soit 1 Mo. En hexadécimal, les adresses possibles vont courir sur 5 chiffres, de 00000h à FFFFFh. Nous appellerons pour l'instant cette adresse complète adresse physique.

Les registres du microprocesseur sont au mieux de 16 bits de largeur, soit 4 chiffres hexadécimaux. C'est le cas du registre IP, qui pointe sur la prochaine instruction à exécuter. Ces 16 bits nous permettent de pointer quelque part dans un bloc de 65536 octets, ou 64 Ko. Les adresses dans ce bloc vont courir de 0000h à FFFFh.

Dans l'espace total adressable, nous pouvons définir 16 de ces blocs, chacun correspondant à une valeur du chiffre hexadécimal le plus significatif, de 0 à F. En d'autres termes, ces 16 blocs commenceront aux adresses 00000h, 10000h, 20000h..., F0000h. Par exemple, le 5 ème bloc comprendra les adresses de 40000h à 4FFFFh.

Pour pointer l'adresse 42DF3h, il faut tout d'abord choisir, d'une façon ou d'une autre, la page 4, par exemple en déposant préalablement cette valeur dans un registre de page. Il faut ensuite que le mot de 16 bits 2DF3h soit dans un registre, IP par exemple. Si nous considérons que le registre de page pointe vers un début de page, 40000h ici, alors IP représente le déplacement, offset en anglais, à effectuer à partir du début de page pour atteindre l'adresse convoitée.

Adressage par pages
figure 2.04 Adressage par pages [the .swf]

Les 16 blocs occupent tout l'espace mémoire sans se chevaucher. À une adresse physique, correspondent un numéro de page et un déplacement uniques.

Cette méthode serait parfaitement viable. En effet, quand la mémoire est parcourue, le changement de page est une opération beaucoup moins fréquente que les changements de déplacement. Nous pourrions par exemple positionner le registre en début de module, l'évolution de IP ou BX suffisant ensuite pour se déplacer efficacement dans la mémoire. Nous pourrions améliorer encore le procédé en gérant simultanément plusieurs registres de page, pour accéder à des zones spécialisées de l'espace adressable : une page pour le code, une page pour la pile, une autre pour les données, etc.

En réalité, la méthode adoptée par Intel est un peu plus complexe. Néanmoins, ce procédé par pages non recouvrantes en est une bonne introduction. Dans un autre contexte, le registre de page pourrait être extérieur au microprocesseur, faire partie de la circuiterie d'accès à la mémoire, et être positionné par une opération d'entrée/sortie. Un microprocesseur pourrait ainsi accéder à un espace mémoire plus grand que celui pour lequel il a été initialement dessiné, au prix il est vrai de beaucoup de complications. C'est la base de la mémoire étendue LIM/EMS.

Venons-en maintenant à la solution réellement utilisée dans le 8086/8088, proche de celle que nous venons de décrire, troublante au premier abord mais plus efficace. Il existe effectivement dans ce processeur un certain nombre de registres, non pas de page mais de segment  ; l'adresse physique sera également toujours le résultat d'un déplacement, contenu dans un registre comme IP ou BX, à partir du début de cette zone appelée segment.

Alors que 4 bits suffisaient pour définir une page, les registres de segment sont, comme les autres registres généraux, de 16 bits de largeur. Il est donc possible de définir 65536 segments différents.

L'adresse physique d'un début de segment s'obtient en ajoutant 0h, soit 0000b, à droite de la valeur du registre de segment considéré. En d'autres termes, on multiplie cette valeur par 16, ou on la décale de 4 bits vers la gauche, et on complète par des 0. Les débuts de segments sont les adresses divisibles par 16.

L'adresse physique d'un accès mémoire se calcule toujours en additionnant l'adresse physique du début de segment au déplacement sur 16 bits.

Adressage segmenté
figure 2.05 Adressage segmenté [the .swf]

Nous voyons que nous pouvons placer les segments pratiquement où nous le désirons, à 16 octets près.

De cela, nous déduisons trois conséquences liées, pas forcément agréables pour le programmeur :

  Les segments se recouvrent.

  À une adresse physique, correspondent un grand nombre de couples segment/déplacement.

  Le calcul de l'adresse physique, dans le cas général, n'est pas immédiat, comme donc n'est pas immédiat le fait que deux couples représentent la même adresse physique.

Nous pouvons nous étonner de ce choix d'un si grand nombre de segments possibles. N'aurait-il pas été préférable de réduire le recouvrement intersegment pour laisser la porte ouverte à des bus d'adresses de 24, voire de 32 bits ? D'un autre coté, le fait de pouvoir placer les segments avec une telle liberté permettait d'optimiser l'utilisation de la mémoire, à une époque où celle-ci était rare et chère. Sauf erreur, les premiers IBM PC étaient proposés dans une version n'embarquant que 16 Ko (oui, 16384 mots de 8 bits).

Nous devons maintenant répondre à la question de savoir quelles valeurs de segment et de déplacement va utiliser la BIU pour élaborer l'adresse physique. La réponse, de bon sens, nous est en grande partie fournie par le modèle du programmeur et les noms qu'y portent les divers registres (voir plus loin). Nous verrons avec le jeu d'instructions, et plus précisément les modes d'adressage, les règles de construction par défaut et par surcharge de l'adresse physique, pour tous les processeurs de la gamme.

Il est par exemple logique que l'adresse générée pour charger une instruction sera située au déplacement IP (Instruction Pointer), dans le segment CS (Code Segment).

Dans le 8086, quatre registres de segment sont à notre disposition :

  CS, Code Segment, associé à IP.

  SS, Stack Segment, associé à SP, BP.

  DS, Data Segment, associé à SI.

  ES, Extra Segment, associé à DI.

Spécificités des segments de registres

Segment

Type

Règle implicite

CS, Code Segment

Instructions

Toute référence à du code.

DS, Data Segment

Données

Cas général des références à des données.

ES, Extra Segment

Données

Destination des opérations sur les chaînes.

SS, Stack Segment

Pile

Toutes références à la pile.

Que signifie ce tableau ? Tout simplement qu'un JMP offset fabriquera l'adresse de destination à l'aide de offset et de CS, MOV EAX, [offset] transfèrera dans EAX la donnée située à l'adresse fabriquée à l'aide de offset et de DS.

Ces associations sont par défaut, et peuvent être surchargées, c'est-à-dire forcées par le programmeur. C'est ce que les documentations appelles une surcharge de segment, ou segment override .

Remarque

Code relogeable

La segmentation permet d'écrire facilement du code relogeable, et même dynamiquement relogeable. Il suffit qu'un programme ne modifie pas la valeur des registres de segment, qu'il se contente d'accepter leur valeur, ce qui est normalement le cas. Le système d'exploitation peut alors placer le code et les données pour optimiser l'occupation de la mémoire, et en fonction de la quantité de mémoire installée. Il suffira ensuite d'initialiser de façon adéquate les registres de segment, pour que le programme s’exécute sans modification du code machine à son emplacement effectif. Les éléments pour lesquels la valeur du segment importe seront cités dans des tables, dites tables de relocation, pour être initialisées au lancement du programme.

Plus précisément, sous DOS, le programme est fabriqué en supposant CS à 0000h et en exprimant les autres registres de segment par rapport à CS (travail du lieur). Au chargement parle loader de DOS, il suffit d'ajouter systématiquement la valeur de CS à tous les registres de segment.

Pour exprimer une adresse physique, dans du code source ou du texte libre, la syntaxe SSSS:DDDD est universelle. DDDD représentent la valeur du segment, DDDD le déplacement. Il est rare d'utiliser la valeur du segment. La mention du registre suffit généralement, CS:DDDD. Le segment est même souvent omis, quand le registre par défaut est utilisé.

Remarque

NEAR, FAR, SHORT

Dans les instructions de saut (au sens large) va apparaître la notion de distance ou de proximité. Si un saut se fait avec changement de segment, il s'agira d'un saut lointain, ou FAR . Dans le cas contraire, d'un saut proche ou NEAR . Un saut NEAR qui de plus peut se coder sur 8 bits signés relativement à l'adresse actuelle est un saut court, ou SHORT . Ceci concerne CALL, JMP, Jcc et sera étudié en même temps que ces instructions.

D'une façon générale, ce qui est NEAR ou SHORT est relogeable, ce qui est FAR ne l'est pas.

Le sommateur situé en haut de la BIU peut maintenant être précisé :

Fabrication des 20 bits d'adresse
figure 2.06 Fabrication des 20 bits d'adresse [the .swf]
La queue de préchargement

Nous avons vu d'une part que les accès mémoire, par la BIU, n'étaient pas immédiats, et qu'il existait même des mémoires lentes, demandant au processeur de temporiser durant un certain nombre de phases d'attente TW. D'autre part, certaines instructions traitées par l'EU pouvaient être très longues.

L'architecture interne a été optimisée, afin d'éviter autant que possible que les deux unités fonctionnelles ne passent leur temps à s'attendre mutuellement.

Le premier point à respecter a été de rendre les fonctionnements de l'EU et de la BIU aussi indépendants que possible. Il est par exemple impératif que l'EU soit autonome lors du traitement d'une instruction longue, qu'elle n'ait pas à solliciter la BIU pendant cette durée. Les deux unités sont asynchrones, et ne se rencontrent que de temps en temps.

Ensuite, afin que la BIU fonctionne au maximum de ses capacités et de celles de la mémoire, qu'elle profite au mieux du temps laissé libre par l'EU, il lui a été laissé la possibilité de s'avancer dans son travail, sous la forme d'une file d'attente ou queue d'instructions. En fait d'instructions, il s'agit de code, opérandes compris. Dans MOV EAX, 12h, la valeur numérique 12h fait partie du flux d'instructions. Cette queue est un registre de 6 (4 pour le 8088) octets, qui se remplit par une extrémité pour se vider par l'autre. On parle de pile FIFO (first in, first out, soit premier rentré, premier sorti). L'image d'une pile n'est pas bonne ; celle d'un tube convient mieux. Cette configuration est connue sous le nom de structure pipeline .

Nous trouvons souvent sur la documentation en langue anglaise le mot fetch. Il pourrait se traduire par aller chercher, dans le sens d'un approvisionnement. C'est le travail de la BIU. Appelons cette action accède , comme nous appelons traite l'action consistant à traiter une instruction et attend le fait d'attendre, faute de munitions généralement.

Représentons chronologiquement l'enchaînement de ces actions :

Enchaînements de tâches
figure 2.07 Enchaînements de tâches [the .swf]

Sur la première ligne, un seul bloc s'occupe de tout ; il enchaîne approvisionnements et traitements, sans attente semble-t-il.

Le second schéma représente le même fonctionnement, dans lequel nous avons graphiquement séparé les fonctions BIU et EU. Nous constatons qu'à chaque instant, l'une des deux unités est au repos.

Dans le troisième schéma, les deux unités ont été rendues indépendantes. Elles peuvent fonctionner simultanément, et un moyen a été donné à la BIU de laisser le résultat de son travail à la disposition de l'EU, par la queue d'instructions. Cette particularité est représentée par une flèche. D'une certaine façon, la BIU anticipe les besoins de l'EU.

Nous voyons que le point le plus important est l'indépendance des deux unités BIU et EU, plus que cette fameuse queue, qui ne gagnerait pas forcément à être plus longue. Il suffit qu'à l'issue du traitement d'une instruction, tous les éléments concernant la suivante soient disponibles.

Le but est que l'EU (et non le BIU, qui n'est qu'un moyen) fonctionne au mieux de ses possibilités.

Nous avons représenté un cycle attend initial comme si nous représentions le début du code ; cela n'a aucune importance. Il arrive que, dans certaines circonstances, l'EU doive subir un ou plusieurs cycles de ce type. Ces cas sont :

  Quand une donnée n'est pas dans l'instruction et ses opérandes, donc pas dans la queue. L'instruction mov ax, word ptr[122h] , par exemple, demande de charger AX par la valeur à l'adresse 122h, ou plus exactement au déplacement 122h du segment pointé par le registre DS. 122h est dans la queue, mais l'EU devra charger la BIU de faire l'emplette de la valeur contenue à cette adresse, et devra se reposer pendant ce temps.

  Dans le cas d'une instruction de déroutement de programme, typiquement un saut. Même un saut non conditionnel va perturber le pipeline. Nous pouvons facilement interpréter cette particularité. La BIU charge dans la queue le code comme il vient, en se fondant très certainement sur la valeur du registre IP. Or, l'instruction de saut inconditionnel, JMP , est équivalente à une instruction de déplacement de données : mov ip, adresse . La BIU ne peut pas en connaître le résultat tant que l'EU ne l'a pas traitée. Il serait justifié de penser que c'est par une comparaison de la valeur effective de IP et de celle que la BIU attendait qu'une procédure de récupération de la queue est déclenchée : purge et fourniture de l'instruction située au bon IP. Remarquons enfin qu'une queue plus longue serait certainement un handicap dans de telles situations.

Nous sommes en 1978, très loin des algorithmes de prédiction de nos processeurs modernes. Néanmoins, la technique est astucieuse, et permet de tirer un maximum de l'EU, sans être trop pénalisée par de la mémoire lente.

Les cycles d'attente dans la chronologie de la BIU, provoqués par exemple par des instructions dispendieuses dans l'EU, sont normaux, le but étant de se mettre à la disposition de l'EU.

Le pipeline du 8086 est rempli par un mot de 16 bits, quelles que soient les instructions lues. Il ne peut donc y avoir de dégradation des performances par désalignement de la mémoire du code. La notion d'alignement n'existe pas pour le code.

 

Vecteurs d'interruptions et de reset

Un certain nombre d'adresses sont réservées aux vecteurs de reset et d'interruptions :

Adresses réservées
figure 2.08 Adresses réservées [the .swf]

Le processus de démarrage, ou de reset, sur le 8086 est très proche du processus générique que nous présentions au chapitre précédent. Le processeur exécute alors du code situé à FFFF0h et jusqu'à FFFFFh. Au vu de la taille de cette zone, c'est généralement un JMP que nous trouvons à cette adresse. Les explications du chapitre 1 sont suffisantes. Le démarrage normal de l'IBM PC ou XT consistait, après une séquence d'initialisations et de tests nommée POST (Power On Self Test), à rechercher un secteur bootable sur une disquette, puis sur un disque dur quand il était présent. Cette zone était nécessairement en ROM. Cette dernière nécessité demeure bien entendu aujourd'hui, au moins à la mise sous tension. En fait, aujourd'hui, un microprocesseur Pentium 4, ATHLON ou ATHLON 64 démarre en mode réel (nous y reviendrons) et donc exactement de la même façon.

En bas de la mémoire, de 00000h à 003FFh, se trouvent 256 blocs de 4 octets ; chacun d‘eux est (ou contient ?) un vecteur d'interruption, c'est-à-dire l'adresse où va démarrer la routine de traitement de l'interruption. Dans chacun de ces blocs, les 2 octets d'adresses inférieures contiennent le déplacement, les 2 autres contenant le segment. C'est tout simplement l'adresse effective ou adresse physique. L'ordre dans lequel ces 4 octets sont implantés en mémoire trouve son explication dans l'Annexe C consacrée aux conventions Little-Endian et Big-Endian. Cette lecture peut sans problème être différée.

Les interruptions sont numérotées de type 0 à type 255, en démarrant par celle dont le bloc-vecteur est à 00000h. Nous écrirons le plus souvent : l'interruption xxh, pour désigner son programme de traitement.

Tous ces vecteurs ne sont pas libres d'utilisation. Certains sont imposés par le jeu d'instruction. Ce sont les exceptions du microprocesseur, c'est à dire des interruptions spécifiques levées par celui-ci quand telle ou telle circonstance inattendue survient. Par exemple, une division par 0 va lever l'exception type 0, Divide error. L'utilisation des exceptions n'est pas nécessairement signe d'erreur, les circonstances peuvent ne pas être si inattendues que cela et leur traitement par exceptions peut très bien constituer la solution la plus souple.

Tout un chacun s'est un jour posé des questions sur le fonctionnement d'un debugger, qui nous donne l'impression que notre machine nous permet de regarder tourner un programme pas à pas, tout en surveillant la valeur de la mémoire et des registres. Deux exceptions facilitent ce tour de magie:

  Type 1, ou Single Step. Quand le flag TF (Trap Flag) est allumé, chaque instruction provoque cette exception. De plus, l'extinction de TF pendant le traitement de l'exception et son réallumage au retour sont automatiques.

  Type 3, ou Breakpoint Interrupt. Cette interruption logicielle INT 3 possède la particularité de se coder sur un seul octet. Ce petit détail est très pratique, puisque ainsi elle peut remplacer n'importe quelle instruction, en fait être placée en début de n'importe quelle instruction. Elle sera utilisée pour placer des points d'arrêt.

Pour la gestion des interruptions externes, une logique de gestion des interruptions, en l'occurrence un PIC (Programable Interrupt Controler) 8259 ou compatible, est pratiquement indispensable.

Quand une interruption masquable survient, le processeur termine l'instruction en cours, puis initie une séquence d'acquittement. Au cours de cet échange (avec le PIC), en deux cycles, un octet va lui être communiqué, correspondant au type de l'interruption. Le processeur va en déduire l'adresse du vecteur. Il suffit de multiplier ce nombre par 4, ou plus simplement de lui rajouter deux 0 à droite. Il va ainsi pouvoir traiter l'interruption selon son type, sans avoir à effectuer d'autres recherches pour en identifier la source.

Dans le cas d'une interruption non masquable (patte NMI), aucune procédure d'acquittement n'est lancée. Le vecteur est toujours le type 2.

Modèle du programmeur

Pour programmer en assembleur, il suffit presque toujours de ne retenir de tout ce qui précède qu'un résumé, le modèle du programmeur . Celui-ci est constitué du jeu d'instructions , que nous n'avons pas encore abordé réellement, et d'une cartographie des registres, y compris le registre d'indicateurs ou flags :

 

Modèle du programmeur du 8086/8088
figure 2.09 Modèle du programmeur du 8086/8088 [the .swf]

 

Au modèle du programmeur du microprocesseur il faut ajouter celui de la machine, ou plutôt du couple machine + système d'exploitation. Ses entrées-sorties bien entendu, mais surtout sa mémoire. Le 8086/8088 pouvait, par ses 20 bits d'adresse, adresser 1Mo de mémoire. A ne pas confondre avec la limite de 640 K0 de la mémoire RAM, due non pas au processeur lui-même mais à la définition de l'IBM PC et à son système d'exploitation. L'ensemble 8086/8088, DOS et IBM PC définit donc un modèle de mémoire qu'il n'est pas encore possible, malheureusement, d'oublier complètement en 2003. C'est ainsi que nous  voyons la mémoire dans certains modes de fonctionnement des machines actuelles.

 

 

Cartographie mémoire simplifiée de l'IBM PC
figure 2.10 Cartographie mémoire simplifiée de l'IBM PC [the .swf]

 

Cette représentation est un exemple maximal. La seule contrainte était d'avoir un peu de RAM à partir de l'adresse 00000h en montant, ce qui permet d'initialiser des vecteurs d'interruption, un peu de ROM à partir de FFFFFh en descendant, à cause du RESET, et quelques kilo octets de mémoire écran. La seule limite incontournable (et encore) du standard DOS est la RAM utilisateur à 640 Ko.

Pour des raisons de technologie, il est rapidement devenu plus économique d'équiper la carte mère de 1 Mo de mémoire vive en 1 ou 4 blocs que d'implanter 640 Ko en 10 blocs de 64 Ko, 40 blocs de 16 Ko semblant irréaliste. En effet, à 9 chips monobits 16Kb par banque comme sur les premiers PC, cela représente 360 boîtiers. Avec 9 boîtiers de 1Mb, de la RAM est présente "sous" la ROM. Elle n'est tout simplement pas adressée, du moins au démarrage. La RAM étant d'accès plus rapide que la ROM, il a été inventé une technique dite RAM shadow . D'une façon que nous ne détaillerons pas, qui est propre au fabriquant de la carte mère et du BIOS, la ROM est recopiée dans la RAM située sous elle, en miroir. Une fois cette opération effectuée, c'est la ROM qui n'est plus adressée, au profit de la RAM. De plus, la zone de la ROM peut perdre son caractère Read Only.

L'extension mémoire EMS

A partir de la généralisation des lecteurs de disquettes, puis avec le PC-XT et son disque dur, la mémoire ROM avait pour seul rôle de gérer le boot en accord avec le matériel réellement installé sur la carte-mère, cette séquence se terminant par le lancement d'un système d'exploitation sur disque ou Disk Operating System. Il est alors clair qu'il n'est plus souhaitable de figer un interpréteur BASIC en ROM, il est bien plus facile de le lancer à partir du disque. Ce qui signifie que la zone mémoire de 384 Ko entre 640 Ko et 1024 Ko sera généralement sous-utilisée. Elle sera mise à profit pour augmenter artificiellement la mémoire vive utilisable sous DOS.

La limite de RAM utilisateur semblait au départ ne pas devoir poser de problème durant la durée de vie du 8086/8088. Ce qui était plutôt sensé: imagine-t-on sérieusement un 8088 à 4,77 MHz traiter efficacement un 1Go de données ? Il ne faudrait pas croire à la naïveté des concepteurs de machines, processeurs et systèmes d'exploitation : tous savaient bien que la limite des 640 Ko serait rapidement contraignante, mais pensaient qu'il serait alors temps de passer à la génération suivante de matériel et d'OS. Au moment de sa présentation, une génération doit apparaître parfaite, voire définitive, puis, dès que le marché commence à s'essouffler, beaucoup d'acteurs de ce marché gagnent à ce qu'elle devienne vite obsolète.

Malgré cela, il a fallu artificiellement augmenter, et considérablement, la mémoire adressable par le 8086. Pourtant, en 1985, au moment où ces extensions deviennent réellement disponibles, le 80286 existait depuis longtemps, et le PC-AT arrivait sur le marché. Si comme nous allons le voir le 80286 adressait 16 Mo de mémoire sans artifice, nous verrons également que éditeurs et utilisateurs choisiront de continuer très longtemps à utiliser DOS, donc d'une certaine façon à n'utiliser qu'un 8086 dans le 80286. La norme d'extension que nous allons aborder était donc plus une rustine à DOS qu'une évolution du 8086.

Les gens intéressés à l'augmentation de la mémoire adressable étaient les éditeurs de logiciels professionnels, parmi lesquels, outre Microsoft, Ashton Tate pour dBase et Lotus pour le tableur 1 2 3. Des éditeurs sont allés jusqu'à proposer sous leur nom des cartes d'extension. L'idée fut donc de trouver une norme matérielle et logicielle d'extension mémoire impliquant un nombre suffisant de partenaires pour en assurer le succès. Ce furent Lotus, Intel et Microsoft, d'où l'acronyme standard LIM, qui décrochèrent la timbale. Les mots standard EMS, comme Expanded Memory System, et mémoire paginée sont également utilisés. D'autres propositions comme EEMS eurent leur heure de gloire, mais contrairement à EMS, ne nous intéressent plus du tout aujourd'hui.

Par contre, ce n'est pas parce que votre machine embarque 512 Mo de mémoire que les extensions mémoire ne vous importent plus : si un programme un peu ancien, par exemple un jeu, demande un gestionnaire de mémoire étendue d'un certain type, il faudra d'une façon ou d'une autre le lui fournir.

Pour étendre la mémoire dans la norme EMS, il faut une carte mémoire pouvant supporter jusqu'à 32 Mo plus quelques circuits dont certains peuvent être assimilés à des registres. Cette carte n'était pas toujours indispensable, si l'on se contentait de 1 Mo installés sur la carte-mère et d'une émulation EMS. Les clones de PC asiatiques pouvaient ainsi annoncer cette quantité de mémoire.

Revenons à notre carte. Il faut dégager un bloc, de 64 Ko dans les premières versions, dans la zone de 384 Ko entre le haut de la RAM normale à 640 Ko et le bas du BIOS. Dégager signifie que rien n'est adressé dans cette zone, qui ne comporte bien entendu pas de mémoire sur la carte-mère. Ce bloc étant lui-même constitué de pages de base de 16 Ko. La carte EMS peut éventuellement être paramétrée pour situer ce bloc un peu n'importe où dans la zone.

Le principe de base est de projeter des blocs de 16 Ko de la mémoire de la carte additionnelle à l'adresse de chacune des pages du bloc, c'est à dire que le bloc est accédé en lecture comme en écriture aux adresses de la page. Ce qui est intéressant, sinon tout ça n'aurait aucun sens, c'est que le programme peut à tout moment, en adressant les registres de la carte, modifier ce mapping, et ainsi accéder à toute l'étendue mémoire de la carte EMS par combinaison de pages successives.

Principe de la projection de pages
figure 2.11 Principe de la projection de pages [the .swf]

La zone des pages dans la mémoire de la CPU, vide en l'absence de carte EMS, s'appelle un cadre de page ou page frame.

Il faut  savoir que les signaux disponibles sur les connecteurs d'extension, nommés slots, des PC sont très nombreux, couvrant l'ensemble des bus et signaux de contrôle. Rappelons que :

  Quand une mémoire est divisée, physiquement ou mentalement, en blocs, un certain nombre de bits de poids faible servent à accéder à un octet au sein de chaque bloc. Ce nombre de bits correspond à la taille du bloc. Les bits restants, de poids fort, servent à déterminer le bloc.

  20 bits correspondent à 1048576 octets, soit 1Mo. 14 bits à 16384 octets, soit 16 Ko. 6 bits à 64, ici pages ou blocs. 25 bit correspondent à 32 Mo et 11 bit à 2048.

Considérons le bloc-page situé de C0000 à C3FFF. En binaire :

1100 00-00 0000 0000 0000 à 1100 00-11 1111 1111 1111

Nous distinguons bien les six bits fixes sur la page, et les 14 bits qui sont entièrement parcourus sur l'étendue de cette page. Donc, programmer la carte EMS pour le bloc C0000-C3FFF consiste à configurer la carte pour valider le bloc souhaité quand les 6 bits de poids fort du bus d'adresses seront à 110000. Il serait possible de traiter moins que 6 bits, par exemple il est déjà certain que le msb est toujours à 1, puisque la zone de 64 Ko est toujours au dessus de 64 Ko. Ce n'est pas de l'électronique bien compliquée, le seul problème venant du fait que sur une carte complète équipable de 32 Mo, il faut adresser 2048 pages possibles. Il n'y aura bien entendu pas un fil qui partira réellement vers chaque page, mais plutôt une boite qui prendra 6 fils en entrée et sortira sur 11 fils.

Deux versions de la norme ont existé, la 3.2 et la 4.0. La 4.0 représente une évolution importante de la 3.2, en termes de mémoire disponible, de positionnement du bloc dans la partie haute de la mémoire, et de nombre de pages. Quand un gestionnaire EMS est installé, sa gestion est possible au travers des fonctions de l'interruption 67h.

Aujourd'hui, point n'est besoin d'installer une carte pour bénéficier de l'EMS. Nos machines contiennent suffisamment de mémoire, mais accessible uniquement en mode protégé. Cette mémoire est rendue accessible en mode réel et v86 (les notions de modes protégé, réel et V86 sont vues un peu plus loin dans ce chapitre) via le gestionnaire de périphérique EMM386. Il s'agit d'un émulateur EMS, accessible donc par les fonctions de l'interruption 67h. Une première partie de ces fonctions est compatible 3.2 (donc 4.0). Les suivantes sont uniquement compatibles 4.0.

La mémoire EMS, le gestionnaire EMM386, concernent la mémoire paginée. Il existe également la mémoire étendue, permettant de bénéficier sous DOS de la mémoire située au dessus du premier Mo, dans les générations qui suivront les 8086 et 80186. Mais là, nous anticipons.

Voilà pour le 8086/8088, un microprocesseur 16 bits quelque peu étrange : bus d'adresse sur 20 bits, registres en 16 bits, un accès mémoire aligné sur l'octet. L'IBM PC avait tout pour décourager le bidouilleur, le coté institutionnel de son constructeur n’arrangeant rien. Mais il a fini par imposer son architecture, peut-être à cause de la puissance d'IBM et d'Intel, mais surtout grâce à la possibilité laissée à tout le monde de proposer des machines compatibles sans royalties, et à l'impact de l'industrie asiatique sur le prix de ces équipements.

Voyons maintenant comment les rejetons successifs de ce microprocesseur ont abouti aux modèles actuels, Pentium 4 et Athlon XP, au moment de l'arrivée sur le marché grand public des modèles 64 bits.

2.2 Du 8086 aux Pentium 4 et Athlon, ...

À partir des 8088/8086, nous allons survoler les macro-étapes qui ont abouti au modèles actuels. Nous ne nous intéressons qu'aux grandes évolutions, de façon plus thématique qu'historique, en acceptant les erreurs de chronologie, et en négligeant les sous-versions, les modèles ou technologies sans descendance, les versions spécifiques aux configurations multiprocesseurs et aux ordinateurs portables pourtant promis à un bel avenir.

2.2.1 Evolution des processeurs: pourquoi et comment ?

Il est permis de supposer que, malgré des rapports parfois tendus, cette évolution implique, en même temps que les fondeurs Intel et aujourd'hui AMD, les concepteurs de systèmes d'exploitation : Microsoft et la communauté Linux, pou ne citer que les deux plus connus du grand-public.

Le moteur de la fuite en avant vers la performance est certainement en partie l'organisation de l'obsolescence rapide du parc. Et sur ce plan, les intérêts des fondeurs, des éditeurs de logiciels et des concepteurs et fabricants de matériel sont convergents. Sans oublier les auteurs et éditeurs de livres d'informatique, la presse spécialisée, etc ... Réellement beaucoup de monde gagne à cette frénésie du renouvellement.

Seul les utilisateurs finaux ne s'y retrouvent absolument pas. Et résister n'est pas facile. En face, c'est la coalition. Votre dentiste, ou votre charcutier, est tout à fait satisfait de son informatique, et ce depuis maintenant quelques années, le fait mérite d'être signalé. Suite à une remarque de son comptable, il rentre en contact avec l'éditeur de son logiciel de gestion, spécifique à sa profession. Celui-ci le convainc de passer à la nouvelle version : mêmes fonctions que la précédente, mais une meilleure adaptation à la législation et une interface un peu améliorée. Accord, contrat, installation : marche pas. Oui, la nouvelle version n'est pas prévue pour fonctionner sous Windows 3.11. Et voilà comment il est indispensable d'acquérir une machine 25 fois plus rapide, pour faire la même chose qu'auparavant.

Dans le monde PC, une contrainte fondamentale de l'évolution est le maintient de la compatibilité ascendante: il faut au minimum que la génération N+1 héberge sans problème les applicatifs les plus récents de la génération N. Ce qui conduit par poupée russe un support souvent correct des plus anciens. Pour ce qui est de l'exemple du dentiste-charcutier, 3.11 et 2000 pro ne sont pas en N et N+1, mais plutôt en N et N+3.

Le premier effet est l'amélioration des performances. Le programmeur devra en tenir compte. Un programme exige généralement une configuration minimale pour tourner, mais il faut également qu'il demeure exploitable sur une machine plus rapide. Cette remarque ne se limite pas aux jeux: le scrolling de Word 2000 ne mériterait-il pas d'être ralenti sur les machines modernes ?

Autre voie majeure de progrès, l'amélioration de la sécurité. Bien entendu, le système d'exploitation y est pour beaucoup. Windows 2000 ou XP seront beaucoup plus tolérants envers nos développements erratiques que ne le sont 95 ou 98. Parce que 2000 et XP exploitent mieux les possibilités d'isolement des tâches mises à disposition par les microprocesseurs.

Un premier type d'évolution concerne peu le programmeur: il s'agit de l'amélioration des performances sans modification du modèle du programmeur. La plus évidente est l'augmentation de la fréquence d'horloge, mais il y eut l'intégration du coprocesseur arithmétique dans le microprocesseur par exemple, et il en existe depuis de nombreuses et de plus en plus subtiles. Citons les caches mémoires de différents niveaux, le traitement en parallèle de plusieurs files d'instructions, lié à des algorithmes de prédiction jouant sur les divers préchargements, etc...

Ce premier type d'amélioration n'oblige pas à réécrire le code, mais peut amener à l'écrire différemment :

Tout d'abord, l'amélioration exponentielle des performances déplace les problèmes. Avec un 8088, ou même un 80386, nous nous dispenserions de certaines gourmandises graphiques, peu utiles au vrai métier du programme. Moyennant quoi, une petite gestion commerciale tournera très bien sur une machine équipée d'un K6, à l'aide de logiciels de la même génération.

Mais c'est surtout en termes d'optimisation que l'évolution se fait le plus sentir. Il est aujourd'hui plus lent d'utiliser l'instruction LOOP que d'écrire soi-même la boucle, à l'aide par exemple de DEC et JNZ. Et encore, parfois l'instruction la plus efficace dépend du constructeur du microprocesseur, en fait Intel ou AMD.

Le temps mis à traiter une routine est mesurable, mais pratiquement plus calculable. Est-on même certain qu'il soit reproductible ? Cette reproductibilité est quoi qu'il en soit remise en cause par le système d'exploitation Windows.

Résumons-nous : pour un premier type d'amélioration théoriquement transparentes, il est surtout important de se documenter. C'est au chapitre de l'optimisation que ces connaissances seront le plus utiles.

Il en va tout autrement pour le second type d'évolution. Il s'agit de modifications du modèle du programmeur. En comparant un Pentium 4, voire un Opteron (64 bits) à un 8088, il est difficile d'imaginer une filiation, et encore moins une compatibilité. Et pourtant...

Parmi ces évolutions sur le fond, il en est une qui frappe immédiatement: le fonctionnement simultané de plusieurs tâches est devenu quelque chose de réellement opérationnel, d'usage courant, même permanent. Qu'il s'agisse d'une machine familiale ou d'un réseau d'entreprise.

Le support sécurisé de plusieurs tâches a été un objectif prioritaire de l'évolution de la structure intime des processeurs. En fait, nous verrons que la tâche est aujourd'hui un objet existant réellement au cœur du processeur.

Le multitâche

Sous DOS, certains artifices bien pratiques pouvaient donner l'illusion que plusieurs tâches pouvaient tourner simultanément. Un spooler d'imprimante, qui va utiliser des interruptions pour travailler en arrière-plan d'une application, un programme de type TSR (Terminate and Stay Resident). D'une certaine façon, les debuggers peuvent donner la même impression.

Si nous voulons aller plus loin dans l'illusion nous pouvons tenter de charger plusieurs programmes en mémoire et de les faire fonctionner à tour de rôle. Cette magie fonctionne mieux si un environnement fenêtré montre les deux applications évoluer de concert.

Il suffit par exemple que chaque programme décide "de temps en temps" de donner la main à une autre application ou le plus souvent au système d'exploitation qui s'occupera de lancer la tâche suivante. Nous venons de définir le multitâche coopératif , qui était utilisé dans Windows 3.1 par exemple. Une tâche pourra par exemple tester en boucle un ensemble de capteurs, dont le clavier, traiter l'évènement s'il y a lieu, et "rendre la main" à la fin de chacune de ces boucles. Si aucune touche ou aucun capteur n'a été activé, la boucle sera très courte, et plus longue dans le cas contraire. Si le traitement est considéré comme trop long, il pourra être prévu d'autres points de commutation, au détriment de la clarté de programmation. Si, suite à une erreur de programmation, évènement non prévu par exemple, la tâche plante (imaginons une entrée en boucle infinie), alors le système est lui-même à la rue. Les autres tâches à ce moment-là en sommeil sont perdues, données comprises. Sans aller jusqu'au plantage, une tâche qui consommerait plus de temps de boucle que prévu va pourrir le fonctionnement de l'ensemble du système. Au chapitre des avantages, nous constatons que chaque tâche "sait" quand elle rend la main. Il n'y a donc pas à se préoccuper particulièrement de sauvegarder le contexte: instruction en cours, valeur des registres, etc ...

Vous pouvez être appelé à mettre en oeuvre un multitâche de type coopératif sur une carte à microcontrôleur. Il sera alors possible d'améliorer considérablement la fiabilité en utilisant un chien de garde , ou watchdog . Cette fonctionnalité est en général intégrée au microcontrôleur. Imaginons un circuit tout simple qui une fois activé ou réactivé va générer un signal de reset au bout d'un certain délai, disons 1 seconde. Il faut pour que cela fonctionne qu'à chaque réactivation la temporisation redémarre de zéro. Le but du jeu est que le reset ne soit jamais activé, donc de rafraîchir le watchdog suffisamment souvent. Dans notre exemple, une réactivation dans la routine de commutation permettrait de s'assurer qu'aucune tâche ne garde la main plus d'1 seconde. Si le watchdog est déclenché, le signal de reset sera mis à profit pour redémarrer à chaud la carte, avec conservation des variables et peut-être continuité des tâches non fautives, et tout traitement jugé adapté à la situation. L'utilisation de chiens de garde est systématique en automatisme, sans même parler de multitâche. Très souvent, le programme est organisé au sein d'une boucle permanente de lecture des capteurs et écriture des actionneurs. Il est courant de donner une limite au temps passé à parcourir cette boucle. Nous avons évoqué le watchdog ici pour des raisons de facilité, mais ce n'est pas une technique propre au multitâche coopératif, ni même au multitâche tout court.

Pour nous affranchir des défauts de ce mode coopératif, testons une autre méthode: programmons un noyau de gestion des tâches. Outre les fonctions de chargement et de lancement des applications, c'est là que va se décider le temps accordé à chacune d'entre elles, selon des critères variés de priorité. Imaginons que sur notre système, rien n'est spécialement prévu pour cette gestion du découpage des tranches de temps. Par contre, nous disposons d'un timer capable de générer une interruption non masquable (NMI) au bout d'un délai programmable à partir de l'action de validation du timer (autorisation de comptage). Eh bien, nous allons écrire au minimum la partie découpage du temps de notre noyau dans la routine de traitement de cette NMI. Juste avant d'accorder un laps de temps à une tâche, nous allons lancer le timer programmé pour la tranche de temps choisie, puis sauter dans la tâche. Et ainsi de suite. Chaque tâche sera stoppée automatiquement à la fin de la période allouée, de façon transparente pour elle. Au noyau de veiller à ce qu'elle retrouve son contexte de fonctionnement quand, après un tour complet, il lui accordera à nouveau du temps. Bien entendu, les choses ne sont pas tout à fait aussi simples, mais il est possible de faire la manip, en prenant soin par exemple de récupérer sur la pile l'adresse de retour et de la remplacer par l'adresse en cours sauvegardée de la tâche suivante, etc ... Ce fonctionnement, dans lequel le noyau du système d'exploitation impose sa volonté, décrit le multitâche préemptif , utilisé dans les versions récentes de Windows.

Le premier gros avantage de cette méthode est qu'une application mal écrite, qui rentre en boucle infinie, ne consommera sur le système que le temps prévu. Si nous pouvons renvoyer le traitement de certaines exceptions inattendues vers le noyau, ce sera encore mieux. Pour "tuer une tâche", le noyau pourra ne plus lui redonner la main, libérer son espace mémoire et enfin libérer les ressources du noyau propres à cette tâche, par exemple les sauvegardes de contexte.

Nous pourrions aller plus loin, mais là la programmation ne suffirait pas, en intercalant sur le bus d'adresse une circuiterie de translation. Il serait ainsi possible que les adresses physiques à la sortie du processeur pointent alternativement vers diverses zones ou boîtiers mémoire. Ainsi, chaque tâche pourrait fonctionner réellement comme si elle était seule, sans se préoccuper de la gestion mémoire. De plus, les applications ne pourraient physiquement pas écrire dans les zones affectées aux autres applications et surtout au système d'explication.

Si chaque tâche voit l'ensemble de la mémoire adressable, il est clair que la quantité de mémoire réellement mobilisée à un instant donné va pouvoir dépasser la mémoire physique disponible sur le système. Il serait bon que de la mémoire rarement utilisée, ou inutilisée pendant un certain laps de temps, puisse être facilement sauvegardée temporairement sur un disque. Cette technique est connue sous le nom de mémoire virtuelle : avec des ressources suffisante et un système d'exploitation le permettant, le programmeur va pouvoir utiliser de façon transparente une quantité de mémoire dont il ne dispose pas physiquement.

Dans ces conditions, chaque tâche d'une certaine façon ne sait pas qu'elle est interrompue de temps en temps, nous ne sommes pas loin de pouvoir faire tourner des applications non prévues pour un mode multitâche: nous avons plusieurs ordinateurs indépendants dans la même machine.

Pour en arriver là, le travail fait par le noyau à chaque commutation est important, en termes de sauvegarde et restauration du contexte de travail et de l'espace mémoire. Nous n'avons pas abordé le problème des ressources partagées: il n'existe pas une imprimante ou un lecteur de CDRom pour chaque tâche. Donc, nous introduisons la notion de jeton ou de sémaphore. Par exemple, une case mémoire accessible depuis toutes les tâches, indiquant la disponibilité de la ressource. Mais tout cela va encore se compliquer. Pour utiliser une ressource, imaginons la séquence:

Je lis le jeton de la ressource (instruction de lecture).

Je constate qu'il est à "libre" (instruction de test).

Je le mets à "occupé" (instruction d'écriture).

Je travaille avec la ressource (plusieurs instructions).

J'ai terminé, je remets le jeton à "libre" (instruction d'écriture).

Rappelons que quand survient une interruption, l'instruction en cours se termine. Si la commutation de tâches survient entre le début et la fin de 2, et si une autre application initie la même séquence pendant la période de sommeil, deux applications vont partager la ressource de façon erronée, et sans le savoir. Le problème pourrait être résolu par une nouvelle instruction qui remplacerait 1-2-3: Lire jeton et changer si ... De plus, il serait bon que cette instruction soit protégée d'une prise de possession du bus durant son déroulement, puisque cette prise de possession n'attend pas la fin de l'instruction en cours.

Nous imaginons facilement que, si notre but est la stabilité du système, il n'est pas souhaitable que les applications ordinaires puissent bricoler le gestionnaire de mémoire, modifier le vecteur de l'interruption de commutation de tâches, etc ... Il faudrait que le noyau puisse avoir ses ressources propres et les protéger, tout comme il faudrait qu'il puisse protéger les ressources de chaque tâche.

Pour fixer les idées, un programme A et un programme B fonctionnent par tranches de 100 ms, et les passages de A vers B et de B vers A prennent chacun 50 ms:

Enchaînement de tâches
figure 2.12 Enchaînement de tâches [the .swf]

Puisque chaque tâche fonctionne le tiers du temps (100 ms sur 300 ms), et si nous sommes sur un système "tournant à 600 MHz", chaque tâche verra virtuellement le même processeur, mais à 200 MHz.

En commutant les tâches plus fréquemment, peut-être l'illusion de simultanéité sera-t-elle meilleure. Malheureusement, ce n'est pas en diminuant le temps accordé à chaque tâche que le temps nécessaire à la commutation va diminuer. Le schéma devient donc:

Enchaînement de tâches
figure 2.13 Enchaînement de tâches [the .swf]

Dans ce cas, un simple règle de trois permet d'affirmer que des 600 MHz du processeur, chaque tâche ne verra que 50 MHz virtuels. 500 MHz seront utilisés par la commutation de tâches.

Il faudra donc trouver un compromis pour déterminer la fréquence de commutation des tranches de temps. De plus, il devient clair qu'un processeur puissant est une des nécessité pour obtenir un multitâche performant. Ce type de fonctionnement demande de la puissance pour la partager entre les applications concurrentes, mais également pour se gérer lui-même.

Cette représentation de la répartition des temps de travail en tranches égales n'est en fait pas réaliste du tout. Supposons que nous avons déposé la calculatrice sur le bureau de Windows, à toutes fin utiles. Hors utilisation de cet accessoire, le mieux serait que nous demandions au noyau de Windows de gérer lui-même les quelques évènements susceptibles de réveiller l'application. Nous sommes là dans le domaine de la programmation par messages et évènements, mais nous devons retenir que multitâche préemptif ne signifie absolument pas tranches de temps égales.

Nous pouvons donc résumer les qualités demandées à un microprocesseur moderne pour faciliter la gestion fiable de tâches simultanées:

  Une puissance de traitement plus importante qu'en monotâche, et même plus importante que la somme des puissances nécessaires à chaque tâche.

  Une gestion de la mémoire qui facilite la protection des tâches entre elles et celle du système d'exploitation.

  Une gestion transparente de la mémoire virtuelle sur disque.

  Réservation d'un certain nombre de possibilités, instructions en particulier, à un niveau de privilège réservé au système d'exploitation.

  Tout ce qui peut rendre plus rapide la commutation de tâches, en terme de sauvegarde et restauration du contexte, sera bienvenu.

  Des instructions de synchronisation entre tâches, comme une lecture/test/écriture de jeton qui ne peut être interrompue.

Deux remarques pour terminer cette excursion dans le monde du multitâche:

Le fait d'utiliser plusieurs processeurs sur la même carte mère augmente certes la puissance globale, dans des proportions parfois décevantes, mais ne simplifie pas vraiment la gestion des tâches concurrentes.

L'artillerie destinée à faciliter le travail du noyau dans sa gestion des tâches pourra être utilisé pour améliorer les performances de chaque tâches, par l'utilisation de fils d'exécution indépendants au sein de la tâche : le threads.

2.2.2 L'évolution

Course à la puissance et à la sécurisation, dans le cadre d'une optimisation du multitâche, c'est ce qui a sous-tendu l'évolution que nous allons maintenant survoler. Les notions évoquées ne seront expliquées qu'au paragraphe suivant, mais ce que nous venons de voir doit nous permettre de comprendre de quoi il s'agit.

La préhistoire : les processeurs 8086/8088 avaient leurs ancêtres chez Intel, qui nous intéressent peu, puisque en dehors de la lignée de compatibilité :

  4004, 4 bits et 4 Ko de mémoire adressable.

  8008, 8 bits et 16 Ko de mémoire adressable.

  Enfin, en 1973, le 8080, 8 bits et 64 Ko de mémoire adressable, horloge à 500 KHz, porté par la système d'exploitation C/PM. Il fut cloné en 1976 par un de ses concepteurs pour donner naissance au célèbre Z80 de Zilog, puis amélioré en 1977 en 8085.

Donc, plus que les ancêtres, c'est la progéniture qui va nous occuper maintenant.

80186 / 80188

Nous avons déjà évoqué les légères améliorations du jeu d'instructions et l'existence de versions dotées de circuits périphériques intégrés. Mais surtout, c'est l'apparition d'un embryon de tolérance d'erreur qui intéresse le programmeur: dans le 8086/8088, une instruction non implémentée n'était pas gérée et provoquait le plantage. Dans le 80186/80188, il est possible d'intercepter cette exception, et par exemple de simuler une instruction inconnue du processeur. Un programme écrit pour le 80186/80188 comportant des instructions spécifiques à ce processeur va planter sur un 8086/8088. Bien entendu, gérer cette exception sur le 80186/80188 ne résout rien pour le 8086/8088, mais prépare l'avenir. Peut-être la notion de lignée n'avait-elle pas été envisagée dès le début de l'aventure.

Pas de 80187, le 8087 reste le coprocesseur mathématique de l'ensemble de la gamme.

Le 80286

Le 80286 apparaît sur le marché en 1982. Pas de version à largeur de bus 8 bits, donc pas de 80288. Ce n'est que trois ans plus tard, en 1985, qu'IBM propose le PAT, motorisé par ce processeur. Entre temps était apparu le PC-XT, version du PC initial supportant un disque dur (10, voire 5 Mo au début), et le marché du clone, monté ou par éléments, le plus souvent d'origine asiatique, avait explosé.

A partir de maintenant, nous simplifions la dénomination des processeurs: plus de 80286, mais des 286, 386, etc... La compatibilité à assurer pour le 286 est avec le 8086. Parler ici de 8088 n'aurait pas de sens, puisque les bus de données font et feront au minimum 16 bits de large.

Le 286 introduisit des améliorations majeures. Il fut de plus supporté par un véritable standard de machine, le PC-AT. Il avait donc tout pour imposer ses améliorations. Mais Intel avait certainement, peut-être pour la dernière fois, sous-estimé les problèmes de compatibilité. Le logiciel disponible sous DOS était déjà très fourni. DE plus, le 286, une fois en mode protégé, ne pouvait facilement revenir au mode réel (voir plus loin). Cela eut pour conséquence que les programmes exploitant le mode protégé furent rares. A l'exception d'OS/2 et d'un Windows 286 qui n'a pas laissé d'inoubliables souvenirs. Le 286 fut donc essentiellement exploité comme un 8086 légèrement amélioré et plus rapide, mais necessitant un gestionnaire de type EMS pour accéder à la mémoire au dessus de 1 Mo. Etrange, pour un processeur dont le bus d'adresse avait une largeur de 24 bits.

A côté des instructions système nécessaire à la gestion des modes et de la protection, propres au nouveau modèle du programmeur, le 286 a vu l'apparition des instructions suivantes, utilisables dans les deux modes: BOUND, ENTER, LEAVE, INS, OUTS, PUSHA, POPA. De plus, les instructions de rotation et de décalage introduisent une amélioration minime.

Le coprocesseur 80287 était une version du 8087 adaptée aux fonctionnalités nouvelles du 286, mais ne proposait pas de différence méritant d'être signalée.

Les processeurs précédents avaient un bus d'adresse de 16 bits, qui permettait d'adresser 1 Mo, un PC sous DOS ne permettant que 640 Ko de RAM. Le bus d'adresse du 286 comporte 24 bits, l'espace adressable est donc physiquement de 16 Mo.

Le 286 est théoriquement pleinement compatible 8086. En fait, avec ce modèle, Intel inaugure une série de "deux en un". Dans un mode de fonctionnement dit mode réel , c'est un 8086.

Mode réel vient de l'anglais real-address mode, pour signifier que la valeur contenue dans les registres de segment était, une fois multipliée par 16 et comme pour le 8086/8088, l'adresse du début du segment. Donc, dans ce mode, il n'accède comme le 8086 qu'à 1 Mo de mémoire. 4 bits du bus d'adresses ne servent à rien.

C'est mode réel que le processeur démarre. Il est ensuite possible de passer dans le second mode, le mode protégé .

Le modèle du programmeur est très proche de celui du 8086. Voyons surtout les quelques différences:

Modèle du programmeur du 286
figure 2.14 Modèle du programmeur du 286 [the .swf]

Les registres et flags supplémentaires ne sont utiles qu'en mode protégé, ou en mode réel au moment du passage en mode protégé. Trois points distinguent ce mode :

  L'espace mémoire adressable est de 16 Mo.

  Existence de plusieurs niveaux de privilège. Certaines instructions, l'accès complet à la mémoire et à sa gestion, à certaines ressources, est réservé au niveau le plus privilégié, qui va ensuite décider des droits des applications.

  Le registre de segment existe toujours, et il est utilisé pour la détermination de la partie haute de l'adresse physique. Mais son contenu n'est plus l'adresse réelle d'un début d'un segment, ni même d'une zone : c'est un sélecteur, qui permet d'accéder à un descripteur. Ce descripteur indique, outre l'adresse et la taille du segment, un certain nombre s'attributs propres à ce segment, dont le niveau de privilège :

Un descripteur de segment sur le 286.
figure 2.15 Un descripteur de segment sur le 286. [the .swf]

Le registre d'état machine MSW ne contient pour le 286 que 4 bits documentés. Trois concernent le coprocesseur arithmétique. Seul le bit 0, ou bit PE comme Protection Enable, nous intéresse, puisqu'il indique le mode de fonctionnement, réel ou protégé. Il est à 0 à la mise sous tension, ce qui correspond au fait que comme signalé le 286 démarre toujours en mode réel. Il faut utiliser l'instruction LMSW (Load Machine Status Word) pour allumer le bit PE et passer en mode protégé. Une fois dans ce mode, le repositionnement à 0 de PE est impossible. Repasser en mode réel ne semble pas avoir été envisagé, et il fallait recourir à une ruse pour ce faire: déclencher un reset et faire en sorte que le BIOS ne réinitialise pas tout. Une opération lourde, qui interdit un multitâche préemptif performant si toutes les applications et le noyau ne sont pas dans le même mode.

Il a existé des versions d’origine non Intel pour le 286, mais il s’agissait généralement de fabrications sous licence, autrement dit de clones légaux.

Le 286 a donc introduit des concepts fondamentaux, encore valables aujourd'hui. Mais en conservant des registres 16 bits. Il demande donc une documentation spécifique, qui serait certainement inutile. Passons donc à la suite de la saga.

Les 386

Après le mode protégé avec le 286, le 386 a vu une autre évolution majeure, l'apparition des registres 32 bits. La version normale, DX, avait des registres, un bus d'adresses et un bus de données tous en largeur 32 bits. La version économique DX présentait un bus de données réduit à 16 bits. Les registres 32 bits encapsulaient les anciens registres 16 bits.

Avec le 386 est également apparu le mode V86, ou Virtual 8086. Dans ce mode protégé, le processeur peut exécuter des programmes écrits pour le mode réel du 8086. Finis les problèmes de commutation des tâches DOS. Mieux, plusieurs applications DOS peuvent tourner simultanément, dans plusieurs fenêtres, si le système d'exploitation propose cette possibilité bien entendu.

D'autres améliorations fondamentales, particulièrement concernant la gestion de la mémoire, sont apparues avec le 386. Elles concernent essentiellement le niveau système.

Il faut également noter quelques caractéristiques transparentes pour le programmeur qui commencent à apparaître : nombreuses unités de traitement fonctionnent en parallèle, cache inclus dans la puce.

A partir de ce modèle, le décor est planté. C'est le modèle le plus ancien qui pourrait supporter les systèmes d'exploitation actuels. Plus exactement, il en est structurellement capable, mais insuffisant en termes de performances. Le modèle du programmeur est figé, aux jeux d'instructions additionnels près.

Pour ces raisons, se reporter à la présentation générale de l'architecture pour plus de détails, chose vraie également pour les 486 et Pentium.

Avec le 386 sont apparues les premières proposition de solutions compatibles alternatives : parmi d’autres, IBM, Cyrix et AMD, avec l’Am386. Cyrix semblait le plus sérieux concurrent à Intel, et nomenclaturait ses compatibles 386 … 486 !

Les 486

À part quelques instructions supplémentaires et l'intégration du coprocesseur arithmétique, les amélioration du 80486 furent essentiellement du type transparent pour le programmeur : cinq pipelines exécutaient en parallèle cinq instructions à différents stades de cette exécution.

Il est dit qu'un processeur est scalaire quand il effectue une instruction par cycle d'horloge, et superscalaire quand il fait mieux. Donc le mot scalaire ne définit absolument pas une architecture, mais une performance. Certains processeurs, particulièrement RISC à architecture Harvard, sont scalaire par construction. Le 486 est scalaire, mais en boostant un moteur qui ne l'est pas intrinsèquement.

En effet, beaucoup d'instructions s'exécutent en un seul cycle, aidées en cela par le quintuple pipeline, par le cache de niveau 1 de 8 Ko inclus dans la puce, et les pattes prévues pour la gestion d'un cache niveau 2 externe. Les dernières versions du 486 (DX4-100) étaient réellement rapides et sont sorties postérieurement aux premières versions du successeur, le Pentium. Avec le 486 sont également apparues des versions particulières, pour des configurations multiprocesseurs, pour des ordinateurs portables, de gestion de l'énergie. Avec lui ou son successeur apparut également le gestion de la température interne du processeur. Coté copieurs crédibles, on trouvait toujours Cyrix, avec le 5x86, et la gamme AMD Am486.

La famille Pentium - P5 - P6

Ce fut ensuite la sortie du premier Pentium (types P5). A partir de ce moment-là, pour Intel comme pour la concurrence, de plus en plus réduite à AMD, l'évolution est rapide, mais ne touche pas au cœur du modèle du programmeur. Cette évolution se fait selon deux axes :

  Des structures internes créatives, de plus en plus performantes, qui de génération en génération accélèrent le même code, à fréquence d'horloge égale, de façon importante. Et de plus, cette fréquence d'horloge continue à croître régulièrement.

  De temps en temps, une technologie apparaît. Ce sont MMX, 3DNow!, SSE et SSE2, qui se présentent sous la forme d'un jeu de registres et d'une amélioration du jeu d'instructions correspondant. Cette évolution est nettement orientée multimédia.

L'ensemble de ces dernières améliorations, qui ne forment plus à proprement parler une norme mais un jeu d'options, a mis en exergue la nécessité d'une identification facile par logiciel des possibilités du processeur. Dès le 80386, il était possible d'obtenir famille, modèle et sous-modèle du processeur, mais uniquement lors d'un reset. Avec le 80486, est apparue l'instruction CPUID (CPU IDentification), qui est évolutive, et qui va beaucoup plus loin dans la détection des possibilités du processeur. Nous aurons l'occasion d'y revenir plusieurs fois. De même, nous réservons un chapitre aux technologies évoluées MMX, 3DNow!, SSE, SSE2.

Le Pentium, le premier modèle du nom, est sorti dans une ambiance de rumeurs et de fausses nouvelles. On trouve encore sur internet de nombreuses références au scandale du Pentium . Un bien grand mot, pour qualifier un bug, chose assez courante.

Fin 1994 éclata une polémique au sujet d'erreurs dans la FPU des Pentium. En de très rares circonstances, des instructions de division de cette FPU voyaient leur résultat entaché d'une erreur sur la cinquième à la neuvième décimale.

La polémique fut d'autant plus vive que le fondeur avait semble-t-il tout d'abord pensé pouvoir passer le défaut sous silence. Politiquement, au sens industriel du terme, l'annonce du bug est arrivée en terrain bien préparé.

Ce n'est pas le seul bug reconnu sur une série, mais bizarrement, c'est le seul dont il est possible de trouver des traces. Encore aujourd'hui, les compilateurs C++ ou Pascal leaders proposent une option de correction du défaut Pentium FDIV. Nous pouvons tester cette option en C++ Builder sur le code:

double a, b, c;
a = M_PI;
b = 2.4 ; 
c = b / a ;

Le code désassemblé sans l'option est:

00401C4B DD45C8           fld qword ptr [ebp-0x38]
00401C4E DC75D0           fdiv qword ptr [ebp-0x30]
00401C51 DD5DC0           fstp qword ptr [ebp-0x40]

Une fois l'option validée, nous relevons le code suivant:

00401C4F DD45C8           fld qword ptr [ebp-0x38]
00401C52 DD45D0           fld qword ptr [ebp-0x30]
00401C55 803DB434400001   cmp byte ptr [0x4034B4],0x01
00401C5C 7504             jnz +0x04
00401C5E DEF9             fdivp st(1)
00401C60 EB05             jmp +0x05
00401C62 E87C090000       call _fdiv()
00401C67 DD5DC0           fstp qword ptr [ebp-0x40]

Il va sans dire que les processeurs incriminés, aujourd'hui anciens, ont été échangés par Intel, et que cette option n'est pas d'une grande utilité.

Le premier intérêt de ce bug est d'être tout à fait expliqué. Pour accélérer le traitement FPU, l'algorithme de division réelle avait été modifié depuis la version 486. La nouvelle version du microcode exploite une table de valeurs numériques à 1066 entrées, codée en dur. Hors, suite à une erreur, 5 de ces valeurs n'ont pas été "claquées" dans le prototype et sont restées à 0. L'erreur étant rare et faible, elle n'a pas été détectée par les procédures de tests avant mise en production, que l'on peut pourtant supposer drastiques. Il est permis de supposer que l'erreur s'est produite sur des composants de tests de type PLA (Programable Logic Array), et que le masque définitif de production a été fait à l'image de ce réseau.

Il reste de cette affaire quelques plaisanteries du meilleur goût. Un exemple:

Q: Pourquoi le Pentium ne s'est il pas appelé 486?
A: Parce que l'addition de 486 et de 100 sur le premier Pentium donnait 585.999983605.

Q: Combien de concepteurs Pentium sont-ils nécessaires pour changer une ampoule?
R: 1.99904274017.

Q: Comment nomme-t-on une série de FDIV sur Pentium?
A: Un calcul par approximations successives.

Ce premier Pentium était structurellement un double 486. Deux structures de pipeline, nommées u et v, au lieu d'une, un cache de niveau 1 pour les données et un autre pour le code. Les techniques de prédiction des branchements pour l'approvisionnement des caches ont évolué. De plus, en interne, des bus de données de 128 et même 256 bits de large, ainsi qu'un bus externe sur 64 bits.

Le jeu d'instructions MMX, et le nouveaux registres MM0 à MM7 (en fait, les registres de la FPU), caractérisent le dernier P5, appelé Pentium MMX.

Chez AMD, nous trouvons, peu performant et sorti relativement tard, le K5. C’était le premier processeur ayant réellement fait l’objet d’un développement original chez AMD. Ce fut un demi échec, réservé aux configurations économiques.

Le premier de la série P6 fut le Pentium Pro. Trois unités au lieu de deux (3 instructions par cycle). Les instructions sont éclatées en microcode, traitées à part et en parallèle dans plusieurs unités spécialisées (entiers, floats, accès mémoire) de façon asynchrone si possible, puis replacées dans une file d'attente. L'idée est de faire le maximum de choses en parallèle, sur le code et les données préchargés, y compris les prédictions de branchement. Aux caches de niveau 1 se rajoute un cache de niveau 2 de 256 Ko. La coopération entre les unités de traitement, les caches et la mémoire est particulièrement travaillée. Enfin, le bus d'adresses passe à 36 bits (64 Go de mémoire accessibles).

AMD, pas fier certainement du succès mitigé de son K5, avait lors de sa sortie racheté NexGen, qui amenait les bases du K6. Puis ce fut le K6-2, qui proposait le jeu d’instructions 3DNow !, une amélioration de MMX.

 Le Pentium Pro n'implémentait pas MMX. Certainement l'idée que le multimédia et le Pro sont deux choses différentes. Bien entendu, cette idée n'a plus vraiment cours aujourd'hui. Donc, le Pentium II est un Pentium Pro équipé MMX aux mémoires cache plus ou moins doublées.

Passons sur le Celeron (the Castrated One), version économique du Pentium II, ainsi que le Xeon (Pentium II on steroïds), version serveur du même. D'autres versions, en particulier du Celeron, exist

Le Pentium III introduit essentiellement le jeu d'instructions et les registres 128 bits SSE.

Pour en arriver au Pentium 4 . Dans ses versions récentes, début 2003, ses caractéristiques maximales sont :

  Jeux d'instructions : MMX, SSE et SSE2.

  Clock maxi (commercialisée) : 3 GHz (calculs à 6 GHz).

  Vitesse maxi de bus : 533 MHz (ou 400 MHz).

  Cache niveau 1 : 8 Ko

  Cache niveau 2 : 512 Ko

Le Pentium 4 est basé sur la micro-architecture Netburst. En particulier, les unités de calcul travaillent à 2X la vitesse d'horloge (6 GHz !). Pour ce qui est des fréquences d'horloge, de calcul et de bus, elles ne sont données qu'à titre indicatif. Chaque visite sur le site d'Intel apporte de nouvelles informations. Au moment de la rédaction, le constructeur affirme qu'un des caractéristiques du dernier cœur Pentium 4 est sa réserve de puissance en termes de fréquence, le goulet d'étranglement étant certainement l'accès mémoire. Cette réserve de puissance en interne explique sans doute la présence de pipelines à vingt étapes, soit deux fois plus que l'Athlon, le processeur pouvant effectuer beaucoup de travail en parallèle sur le contenu de cette queue.

Pendant ce temps, AMD, depuis le succès du K6-2, avait successivement proposé une série de processeurs tout à fait intéressants, économiques et malgré tout performants, surtout si l’on considère la mémoire DDR supportée. Le dernier produit de la gamme (en oubliant pour l’instant la gamme 64 bits AMD64) est l’Athlon XP +.  Ses caractéristiques, un peu en retard à l'instant t sur le Pentium :

  Jeux d'instructions : MMX, 3DNow! et SSE.

  Clock maxi (commercialisée) : 2,59 GHz (3000).

  Vitesse maxi de bus : 333 MHz (ou 266 MHz).

  Cache niveau 1 : 128 Ko

  Cache niveau 2 : 256 à 512 Ko

Voila où nous en sommes au milieu de l'année 2003. Il est bien entendu difficile de faire des prédictions sur la suite de l'évolution de l'architecture. Au grand bonheur de l'industrie du caoutchouc, nous attendons toujours la voiture sur coussin d'air ou à sustentation magnétique basée sur la supraconductivité, annoncés fermement pour l'an 2000 dans les années 70. Les fréquences aujourd'hui mises en jeu sont tout simplement phénoménales pour un électronicien. Les 6 GHz du core, mais peut-être plus encore les 400 à 800 MHz véhiculés sur des cartes-mères vendues quelques dizaines d'euros.

2.3 L'architecture actuelle

Parler de l'architecture des compatibles PC de la dernière génération cache en réalité au moins trois approches :

  Architecture matérielle : c'est ce que nous venons de toucher du doigt dans le paragraphe précédent, avec les notions de caches, pipelines, microcode. L'architecture interne d'un microprocesseur nous est masquée. Néanmoins, nous pouvons interférer par certaines instructions avec par exemple le comportement des caches. Cette partie de l'architecture est spécifique à chaque microprocesseur, bien qu'il existe des points communs, et il faudra le cas échéant se procurer, généralement par téléchargement, de la documentation chez le constructeur, sous forme de brochures ou mieux de guides d'optimisation.

  Modèle du programmeur d'applications : quand vous programmez de simple applications, par exemple sous Windows, seule une partie du microprocesseur (et de l'ensemble de la machine) est mise à votre disposition. Vous devrez alors travailler avec le modèle du programmeur tel que nous le connaissons pratiquement déjà. C'est un modèle standard, commun à tous les processeurs de la même famille, à quelques détails près.

  Modèle du programmeur système : il s'agit de l'ensemble des caractéristiques du microprocesseur, utiles pour programmer au niveau système. Bien entendu, même en restant au niveau application, il n'est pas inutile d'être curieux. Ce modèle est également standard, puisqu'il doit être respecté par tous les processeur aspirant à la compatibilité.

2.3.1 Modèle du programmeur d'applications

Quand nous programmons de simples applications, sous Windows ou sous DOS, le modèle du programmeur est très simple. Dans le cadre d'une application, nous sommes dans un mode ou dans un autre, nous ne gérons en aucun cas le basculement d'un mode à un autre. Pour simplifier, soit nous sommes soit en mode réel, soit en mode protégé.

Dans le premier cas, le mode réel, nous disposons de l'environnement d'un 8086, quel que soit le processeur réellement utilisé. Notre modèle du programmeur est strictement celui du 8086, que nous connaissons déjà. Ce mode est accessible en bootant sur une disquette DOS, ou en utilisant Redémarrer en mode MS-DOS sous Windows 9x par exemple. Nous pouvons conserver ce modèle pour un avatar du mode protégé, le mode virtual-8086. Il s'agit de l'émulation d'un environnement 8086 dans un contexte protégé, géré par le système. Pour le programmeur, pas, ou très peu, de différences avec le mode réel. Ce sont les fenêtres DOS sous Windows. Avec l'avantage qu'il est possible d'en ouvrir un grand nombre, qui seront indépendantes les unes des autres.

Dans le cas du mode protégé, généralement d'applications 32 bits, les choses sont encore plus simples. Il est en effet très improbable que nous ayons à nous préoccuper du contenu des registres de segments. Il faut dire que sous DOS, en choisissant sous MASM le modèle mémoire adéquate, c'est également facile sur ce plan. Le système met à notre disposition une configuration de la mémoire adaptée à nos besoins. Qu'en est-il de la segmentation ? Ce sujet est une des principales préoccupations du paragraphe suivant. Anticipons un peu : la segmentation en mode protégé n'est plus une nécessité pour accéder à toute la mémoire, c'est un facilité accordée à la gestion de la fiabilité d'un système, particulièrement s'il est multitâche.

Un modèle parfaitement plat existe, mais pas sous Windows : il suffit que tous les sélecteurs pointent vers un descripteur tel que adresse de base égale 0, niveau de privilège égale 0 et limite égale taille maximum de la mémoire, jusqu'à 4 Go. Nous nous retrouverions dans un immense environnement de programme .com (voir chapitre suivant), dans lequel nous devrions initialiser au moins une pile, et gérer nous même l'emplacement du code et des données.

En réalité, sous Windows, nous allons demander au système de nous fabriquer au minimum trois segments, code, pile et données, correspondant à CS, SS et DS. Ce travail de demande sera celui du lieur, sur la base du type de programme que nous souhaitons développer.

Le modèle sera constitué de la cartographie des registres, valable pour tous les processeurs depuis les 386, en y incorporant la FPU, c'est à dire l'ancien coprocesseur arithmétique :

Modèle du programmeur IA-32
figure 2.16 Modèle du programmeur IA-32 [the .swf]

Pour le jeu d'instructions, c'est le jeu complet, instructions en virgule flottante comprises, mais en excluant les instructions privilégiées . Il faut rajouter la liste des flags. Instructions et flags sont au chapitre sur le jeu d'instructions. Enfin, selon votre équipement, ou la machine cible, il faudra la documentation des divers jeux d'instructions supplémentaires utilisés, parmi MMX, 3DNow, SSE. Le modèles du programmeur IA-32 se compose donc d'une documentation de base et d'une série de kits, chacun dédié à une technologie particulière.

2.3.2 Modèle du programmeur système

En abordant le sujet, c'est immédiatement l'image de l'iceberg qui vient à l'esprit : ce qui est visible en programmation normale est avec la complexité du niveau système dans le même type de rapport que les parties visibles et immergées du marin glaçon.

La partie invisible de la programmation
figure 2.17 La partie invisible de la programmation [the .swf]

Dans le schéma de l'architecture système IA-32, qui représente de façon simplifiée les registres et structures mises en oeuvre, nous avons marqué d'un astérisque les quelques éléments que reconnaît le modèle du programmeur d'applications :

L'architecture système IA-32
figure 2.18 L'architecture système IA-32 [the .swf]

 

Rappelons les rôles dévolus à la couche système de l'architecture :

  Gestion de la mémoire.

  Protection du système et des applications.

  Multitâche.

  Gestion des exceptions et des interruptions.

  Gestion des caches.

  Gestion du matériel et de l'énergie.

  Déboguage.

  Contrôle et mesure des performances.

  Gestion du mode multiprocesseur.

Ce sujet est développé dans le chapitre suivant, dont la lecture peut très bien être reportée à beaucoup plus tard, pour aborder maintenant la seconde partie et le chapitre Premiers programmes .

2.4 Les architectures 64 bits – AMD64

Ce paragraphe doit être abordé en tenant compte de sa date de rédaction, à savoir fin mai 2003. En espérant que l'avenir ne le rendra pas trop risible.

Depuis plusieurs années, la règle semblait être : Intel développe une nouvelle génération de processeur dans la lignée x86,  AMD la clone, avec de plus en plus souvent des améliorations significatives puis se positionne au mieux sur le marché en travaillant sur les prix. Le terme clonage n'est pas péjoratif, il s'agit simplement de respecter les contraintes de la compatibilité.

Il est possible que, dans le cadre des compatibles PC les plus largement distribués, les choses soient en train de changer. Intel propose depuis déjà assez longtemps l'architecture IA-64 , processeurs Itanium , avec de bons résultats dans le domaine des serveurs, stations de travail et calcul intensif. Malheureusement, cette technologie est peu compatible IA-32, chère et plus lente que IA-32 en mode compatibilité.

Un peu plus tard, sous la forme d'une architecture initialement nommée x86-64, AMD a présenté une solution de compatibilité "à la Intel", en poupée russe, c'est à dire que IA-32 est présent au sein de x86-64, comme le 8086 est présent au sein de IA-32. Ce qui implique que le 8086 est présent au sein de x86-64.

Au printemps 2003, AMD change le nom de la technologie de x86-64 en AMD64 . Nous saurons dans très peu de temps si cette architecture doit s'imposer dans le plus grand public, mais tout, en particulier les possibilités multimédia, porte à le croire.

Pour l'heure, en technologie 0,13µ, l'architecture porte le nom de Hammer (marteau). Elle est déclinée en une version SledgeHammer (masse) orientée serveur, optimisée pour des configurations multiprocesseurs, et une version mono-processeur ClawHammer (marteau de charpentier). Les processeurs portent les noms Opteron et Athlon 64. La sortie réelle de ce dernier modèle est prévue pour l'automne 2003. Une génération 0,09 µ, cœurs Athens, San Diego et Odessa, imminerait.

En termes de logiciel, une version de SuSe Linux adaptée au serveur Opteron est disponible, Windows XP et 2000 dans leurs versions serveur 32 actuelles, non modifiées, ont été testées avec succès par Microsoft, et des version spécifiques de Windows .net  serveur et XP sont en développement.

Tout semble donc bien se présenter pour qu'AMD soit, le temps d'une génération au minimum, en position d'initiateur. Il reste à souhaiter que l'offre de support et de documentation soit d'une qualité au moins équivalente à ce que propose son concurrent. Il semble que ce soit le cas. En particulier, vous trouverez sur le CD-ROM des liens pour télécharger un excellent AMD64 Architecture Programmer’s Manual en 5 volumes PDF, ainsi qu'un tout récent Software Optimization Guide for AMD Athlon 64 and AMD Opteron Processors . Ces documents doivent être utilisables en IA-32 (AMD et Intel), mais il faudra déterminer jusqu'à quel point.

Ces processeurs sont prévus pour faire fonctionner des applications 16 et 32 bits sans recompilation. Ils sont de plus aptes, toujours sans recompilation, à supporter les systèmes d'exploitation 32 et même 16 bits. En survolant la documentation AMD, il apparaît que la transition puisse se faire en douceur, au niveau des applicatifs bien entendu, mais également des systèmes d'exploitation, drivers, et même du BIOS.

La compatibilité des registres est assurée sans surprise. Les technologies MMX, 3DNow, SSE et SSE2 sont présentes, quelques registres supplémentaires étant ajoutés. Les registres 32 bits sont étendus à 64 bits, selon la méthode déjà évoquée des poupées russes, dans la continuité du passage de 16 à 32 bits :

Les registres de l'architecture AMD64
figure 2.19 Les registres de l'architecture AMD64 [the .swf]

Le jeu d'instruction est bien entendu un surensemble de IA-32.

En revanche, au niveau des modes, cela va encore se compliquer, bien que le parallélisme avec l'évolution précédente demeure.

  Un mode long , le système d'exploitation sera obligatoirement 64 bits, et travaillera dans un sous-mode parmi deux :

  Le mode 64 bits, qui utilisera toutes les caractéristiques nouvelles de l'architecture, mais exigera des programmes 64 bits, c'est-à-dire au minimum recompilés.

  Le mode compatibilité, qui exécutera les anciennes applications 16 et 32 bits, sans utilisation bien entendu des nouveaux registres, mais avec semble-t-il un gain en performances.

  Un mode legacy (héritage), qui nous ramène à un processeur 32 bits, qui prendra donc en charge les modes réel, V86 et protégé.

C'est-à-dire qu'a priori, les nouvelles machines devraient être capables de booter sous DOS... Comme pour un Pentium ou tout autre processeur de la lignée, le démarrage ou reset se fait en mode réel, ou plutôt en mode legacy, sous-mode réel.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1