VIII. Assembleur : passage en mode protégé▲
Relu par ClaudeLELOUP.
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.
VIII-1. Histoire de l'informatique▲
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à .
VIII-1-a. L'architecture des premiers Personal Computers▲
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 concurrence est rude. IBM va choisir de fabriquer des ordinateurs de série à bas coût, avec des processeurs qu'elle peut 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égaoctet. Mais c'est une machine 16 bits, ce qui fait qu'elle ne peut représenter que 216 octets, soit 64 kilooctets. 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 leurs 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.
VIII-1-b. Le piège d'IBM▲
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 souci 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 ?
VIII-1-c. Le mode protégé▲
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 processeur 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é.
VIII-2. Problèmes avec 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.
VIII-3. Solutions en mode protégé▲
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.
VIII-3-a. Le GDT (Global Descriptor Table)▲
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 deux nombres dans cet ordre :
- la taille en octets du tableau global des descripteurs, sous forme de mot (16 bits) ;
- l'adresse de début du tableau global des descripteurs, sous forme de double-mot (32 bits), nécessairement.
Comme le monde entier suppose tout à fait intelligemment que si on a une adresse et une taille de tableau à donner, c'est qu'il nous faut le remplir, et que s'il s'appelle "Tableau Global des Descripteurs", c'est qu'il doit contenir des descripteurs, voyons un peu les descripteurs.
VIII-3-b. Les Descriptors▲
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 la structure qui suit.
- Base : adresse linéaire du début du segment, 32 bits : la limite peut être toute la mémoire adressable.
- Limite : taille en octets du segment, 20 bits avec astuce. Normalement, on devrait avoir 32 bits.
- G : drapeau. Si à 1, la Limite est donnée en octets. Sinon, elle est en multiples de 4096 octets. 1 bit. Permet de simuler les 32 bits attendus sur Limite.
- S : drapeau. Si à 1, il s'agit d'un segment système (inaccessible aux autres programmes). Sinon, il s'agit d'un segment de code ou de données. 1 bit.
- Type : type de segment (données, code, etc.). 4 bits.
- DPL : niveau de privilège minimal pour accéder au segment. 2 bits.
- P : Si à 1, le segment est présent dans la mémoire principale. 1 bit.
- D/B : Taille des éléments du segment : opérandes en mode Code, pile en mode Données. 0 : 16 bits ou SP, 1 : 32 bits ou ESP. 1 bit.
- AVL : champ utilisable par le programmeur. 1 bit.
Ce qui fait royalement 63 bits. Un bit ne sert à rien et porte le total à 8 octets, répartis comme suit (attention c'est stéganologique) :
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.
- 0 : Read Only. Lecture seule.
- 1 : Read Only, accessed. Lecture seule, accédé ?
- 2 : Read/Write. Lecture/Ecriture.
- 3 : Read/Write, accessed. Lecture/Ecriture, accédé ?
- 4 : Read Only, expand down. Lecture seule, s'accroît vers le bas.
- 5 : Read Only, expand down, accessed. Lecture seule, s'accroît vers le bas, accédé ?
- 6 : Read/Write, expand down. Lecture/Ecriture, s'accroît vers le bas.
- 7 : Read/Write, expand down, accessed. Lecture/Ecriture, s'accroît vers le bas, accédé ?
- 8 : Execute Only. Exécution seule.
- 9 : Execute Only, accessed. Exécution seule, accédé ?
- A : Execute/Read. Exécution/Lecture.
- B : Execute/Read, accessed. Exécution/Lecture, accédé ?
- C : Execute Only, conforming. Exécution seule, standard.
- D : Execute Only, conforming, accessed. Exécution seule, standard, accédé ?
- E : Execute/Read, conforming. Exécution/Lecture, standard.
- F : Execute/Read, conforming, accessed. Exécution/Lecture, standard, accédé ?
Source : http://www.c-jump.com/CIS77/ASM/Protection/W77_0090_segment_descriptor_cont.htm
Nous avons besoin de trois segments.
- Le segment NULL, nécessaire au processeur en cas d'erreur de segmentation. Tout à zéro, fin du match. gdt: db 0,0,0,0,0,0,0,0
- Le segment de code. Limite à 0xFFFFF, Base à 0, P à 1, DPL à 0, S à 1, Type à Exécution/Lecture accédé, AVL à 1, D/B à 1 et G à 1. gdt_cs: db 0xFF,0xFF,0x0,0x0,0x0,10011011b,11011111b,0x0
- Le segment de données. Limite à 0xFFFFF, Base à 0, P à 1, DPL à 0, S à 1, Type à Lecture/Ecriture accédé, AVL à 1, D/B à 1 et G à 1. gdt_ds: db 0xFF,0xFF,0x0,0x0,0x0,10010011b,11011111b,0x0
VIII-4. De la pratique▲
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.
?fine BASE 0x100
; 0x0100:0x0 = 0x1000
?fine KSIZE 2
?fine 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