<-Précédent | Retour à l'accueil | Contact : etienne"point"sauvage"at"gmail.com | Suivant-> |
Au chapitre précédent, nous avons patiemment retrouvé le contrôle de l'écran, dans ses grandes lignes. A une phase de reconstruction suit une phase de destruction. Il fallait que je m'y colle il y a longtemps déjà, j'ai écumé le net et la documentation AMD, je lis l'anglais comme un américain maintenant, voilà, petits frenchies, voilà où nous en sommes : le mode protégé !
Les plus futés auront remarqué que nous n'avons manipulé jusqu'à présent que des registres de 16 bits. Or, nous avons tous, au moins, des machines 32 bits. Nous avons utilisé des adresses de la forme segment:offset, codées sur 20 bits par une arithmétique assez odieuse, alors qu'un seul registre de 32 bits nous aurait évité cela. Il y avait une raison. Une vraie raison, officielle en diable : pour faire mieux, il faut relever le challenge des débutants, passer en mode protégé. Alors, je vais faire un petit topo sur tout ça.
Un bien pompeux titre, mais ne croyez pas tout ce qu'on vous raconte. Il ne s'agit ici que de prendre en compte de simples considérations historiques qui nous expliquent pourquoi on en est là.
C'est la société IBM (dont le nom signifie quelque chose comme "machines intelligentes pour les affaires") qui a gagné le marché des ordinateurs personnels. N'oublions pas qu'à cette époque (le début des années 1980), les ordinateurs existent, sont assez répandus mais ne sont pas non plus à la disposition de tout un chacun. IBM va jeter les bases de l'informatique personnelle. Bien évidemment, la concurence est rude. IBM va choisir de fabriquer des ordinateurs de série à bas coût, avec des processeurs qu'elle sait produire en nombre.
Et il se trouve que ce sera un succès. Modeste par rapport au nombre d'ordinateurs vendus quotidiennement aujourd'hui, mais suffisamment important pour qu'IBM occupe une grosse part de marché. Les développeurs vont donc fournir des programmes développés pour cette machine, l'IBM PC, basée sur un processeur 8086 (en fait un 8088, mais c'est le 8086 qui a légué son nom à la postérité). Les utilisateurs de machines vont ensuite vouloir faire fonctionner ces mêmes programmes sur d'autres machines. Cela n'est possible que si les machines sont compatibles. Au vu des parts de marché d'IBM, la concurrence est obligée de s'aligner et d'adopter la même architecture que celle d'IBM.
Cette architecture permet d'accéder à 220 octets de mémoire vive, plus ou moins un. Ca fait 1 méga-octet. Mais c'est une machine 16 bits, ce qui fait qu'elle ne peut représenter que 216 octets, soit 64 kilo-octets. Pour adresser 1 Mo, elle doit ruser. La ruse consiste à utiliser deux zones mémoire du processeur pour adresser toute la mémoire. Du coup, on a deux fois 16 bits, ce qui couvre nos 20 bits d'adresse. Oui, mais deux fois 16 bits, ça fait 32 bits, on en a trop. Et c'est à ce moment, je ne sais pas pourquoi mais ils avaient leurs raisons, que les collègues de chez IBM ont décidé que leux deux nombres recouvriraient en partie la zone d'adressage. Il y a 12 bits qui se recouvrent ! 0x1222:2220 correspond à exactement la même case mémoire que 0x1444:0000 ou 0x1000:4440 ou 1111:3330 ! Ce truc délirant, ça s'appelle l'arithmétique des pointeurs.
Peut-être que les gars qui ont pondu ça, ils étaient fatigués, sous pression, les commerciaux leur ont dit que c'était juste un petit truc comme ça, que sais-je. Mais le fait est qu'on a eu l'arithmétique des pointeurs. Et que l'IBM PC a eu un succès fou. Là est tout le drame. Car non seulement la concurrence a dû faire des machines compatibles, mais de surcroît IBM aussi ! Quand l'entreprise a voulu enlever ces zones mémoire qui se recouvraient, par soucis de compatibilité, elle n'a pas pu. Néanmoins, il fallait obligatoirement dépasser cette limite de 1 Mo de mémoire, et quitter cette méchante façon d'adresser les octets. Que faire ?
IBM a inventé le mode protégé. Appelons le mode historique le mode réel. Dans un ordinateur en mode réel, n'importe quel programme peut voir l'ensemble de la mémoire et l'écrire. Cela peut poser des problèmes, notamment quand votre voisin décide d'écrire chez vous. Il faut une astuce pour rendre la chose plus difficile.
Grâce à une série d'instructions à faire dans le bon ordre, de zones mémoire judicieusement choisies et d'un proceseur le permettant (donc au moins un 80286), on peut utiliser 32 bits d'adressage direct et interdire à un programme d'aller voir en-dehors de l'espace qui lui est alloué. C'est cela, le mode protégé.
Il faut s'en douter, si on n'était pas en mode protégé jusqu'à présent, c'est que ce mode pose des problèmes qu'on peut apprécier ne pas avoir. Le plus gros et le plus velu, de mon point de vue, est celui-ci : en mode protégé, adieu les interruptions. Fini les services du BIOS. Plus de changement de mode graphique. Plus d'affichage de caractère, plus de clavier. Plus rien. Il faut tout refaire. Tout.
Bon, ben quand faut y aller, faut y aller.
Techniquement, pour passer en mode protégé, il suffit de ceci :
mov eax,cr0
or ax,1
mov cr0,eax
En langage humain, il suffit de passer le bit n°0 du registre CR0 à 1. Comme le registre CR0 n'est pas éditable par le microprocesseur, on le passe d'abord dans EAX, on fait le OR qui permet de mettre le bit n°0 à 1 sans changer tout le reste, et on remet EAX dans CR0.
Mais ce n'est pas suffisant. En effet, en mode protégé, la mémoire peut être segmentée. C'est un vilain mot qui signifie qu'il faut définir des segments de mémoire. Les anciens registres, tels que CS, DS et ES existent toujours en mode protégé. Ils font toujours 16 bits, mais ce ne sont plus les bits de poids fort de l'adresse. Ils sont devenus des offsets, des décalages. Ils correspondent au décalage nécessaire pour atteindre le descripteur de segment correspondant dans le tableau global des descripteurs, Global Descriptor Table (GDT).
En mode protégé, le processeur va regarder le segment correspondant à son instruction, ajouter cette adresse à son registre GDT, lire le descripteur de segment à cet endroit, en conclure quant à l'adresse concernée et y aller.
Qu'on appelle, en français, le tableau global des descripteurs. C'est une zone mémoire, à une adresse spécifiable comme bon nous semble. Elle est spécifiquement liée à deux instructions particulières, mais une seule nous intéresse ici : LGDT, Load Global Descriptor Table. Elle prend un seul argument, l'adresse (32 bits maintenant, donc) d'une toute petite zone mémoire, que nous allons appeler pointeurGDT:
, comme c'est original. Cette zone contient 2 nombres dans cet ordre :
Oui, les descripteurs. Un descripteur est une structure de données qui décrit, d'où son nom, quelque chose. J'ai parlé avant de segments en mémoire, et bien mettons les deux ensemble : dans le tableau global des descripteurs, les descripteurs décrivent des segments. Il en faut au moins deux : un pour le code, un autre pour les données. C'est comme ça, c'est imposé par le processeur. Par contre, on a le droit de décrire le même segment.
Le descripteur à proprement parler a cette structure :
15 7 0 -------------------------------------------------- | Limite, bits 0-15 | -------------------------------------------------- | Base, bits 0-15 | -------------------------------------------------- | P DPL S Type Base, bits 16-23 | -------------------------------------------------- | Base, bits 24-31 G D/B 0 AVL Limite,fin| --------------------------------------------------
Voici les valeurs possibles de Type:
Nous avons besoin de trois segments :
gdt:
db 0,0,0,0,0,0,0,0
gdt_cs:
db 0xFF,0xFF,0x0,0x0,0x0,10011011b,11011111b,0x0
gdt_ds:
db 0xFF,0xFF,0x0,0x0,0x0,10010011b,11011111b,0x0
On l'a bien mérité, voici le secteur d'amorçage qui passe en mode protégé avant de donner la main à un noyau à venir.
%define BASE 0x100 ; 0x0100:0x0 = 0x1000 %define KSIZE 2 %define BOOT_SEG 0x07c0 BITS 16 org 0x0000 ; Adresse de début bootloader ;; Initialisation des segments en 0x07C0 mov ax, BOOT_SEG mov ds, ax mov es, ax mov ax, 0x8000 ; pile en 0xFFFF mov ss, ax mov sp, 0xf000 ;; Affiche un message mov si, msgDebut call afficher ;; Charge le noyau initialise_disque: ; Initialise le lecteur de disque xor ax, ax int 0x13 jc initialise_disque ; En cas d'erreur on recommence (sinon, de toute façon, on ne peut rien faire) lire: mov ax, BASE ; ES:BX = BASE:0000 mov es, ax xor bx, bx mov ah, 2 ; Fonction 0x02 : chargement mémoire mov al, KSIZE ; On lit KSIZE secteurs xor ch, ch ; Premier cylindre (n° 0) mov cl, 2 ; Premier secteur (porte le n° 2, le n° 1, on est dedans, et le n° 0 n'existe pas) xor dh, dh ; Tête de lecture n° 0 ; Toujours pas d'identifiant de disque, c'est toujours le même. int 0x13 ; Lit ! jc lire ; En cas d'erreur, on recommence ;; Passe en mode protégé cli lgdt [pointeurGDT] ; charge la gdt mov eax, cr0 or ax, 1 mov cr0, eax ; PE mis a 1 (CR0) jmp next next: mov ax, 0x10 ; offset du descripteur du segment de données mov ds, ax mov fs, ax mov gs, ax mov es, ax mov ss, ax mov esp, 0x9F000 jmp dword 0x8:BASE << 4; réinitialise le segment de code ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Synopsis: Affiche une chaîne de caractères se terminant par NULL ;; ;; Entrée: DS:SI -> pointe sur la chaîne à afficher ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; afficher: push ax push bx .debut: lodsb ; ds:si -> al cmp al, 0 ; fin chaîne ? jz .fin mov ah, 0x0E ; appel au service 0x0e, int 0x10 du BIOS mov bx, 0x07 ; bx -> attribut, al -> caractère ASCII int 0x10 jmp .debut .fin: pop bx pop ax ret ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; msgDebut db "Chargement du kernel", 13, 10, 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; gdt: db 0x00, 0x00, 0x00, 0x00, 0x00, 00000000b, 00000000b, 0x00 gdt_cs: db 0xFF, 0xFF, 0x00, 0x00, 0x00, 10011011b, 11011111b, 0x00 gdt_ds: db 0xFF, 0xFF, 0x00, 0x00, 0x00, 10010011b, 11011111b, 0x00 gdtend: pointeurGDT: dw gdtend-gdt ; taille dd (BOOT_SEG << 4) + gdt ; base ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Rien jusqu'à 510 times 510-($-$$) db 0 dw 0xAA55