Precedent | Sommaire | Suivant

4. Passer en mode protege

4.1 Pourquoi passer en mode protege ?

Le microprocesseur d'un PC possede trois mode de fonctionnement : Notre objectif etant de realiser un noyau multi-utilisateur, multi-taches et pouvant adresser toute la memoire, il faut basculer le microprocesseur en mode protege. Ceci a de grosses consequences en terme de programmation. Tout d'abord, le mecanisme d'addressage en mode protege, detaille ci-dessous, est different de celui en mode reel, utilise jusqu'a present. En partie a cause de cela, il ne sera plus possible de s'appuyer sur les routines du BIOS pour acceder aux peripheriques et tous les drivers seront a reecrire.

4.2 Comment passer du mode reel au mode protege

Passer du mode reel au mode protege est tres simple, il suffit de mettre le bit 0 du registre CR0 a 1.
	mov eax,cr0
	or ax,1
	mov cr0,eax		; PE mis a 1 (CR0)
Mais si cela suffit pour changer de mode, cela ne suffit pas a ce qu'un programme continue de fonctionner une fois le mode protege etabli.
Avant de changer de mode, un certains nombre de structures doivent etre correctement initialisees. Ces structures, qui permettent d'adresser correctement la memoire, sont expliquees dans la partie suivante.

4.3 Addressage de la memoire en mode protege

4.3.1 Differents types d'adresses

Il existe pour le programmeur trois adresses : l'adresse logique, l'adresse lineaire et l'adresse physique.

L'adresse logique, composee d'un selecteur de segment et d'un offset, est directement manipulee par le programmeur quand il veut acceder a un endroit particulier de la memoire. Le selecteur permet de pointer sur un bloc de memoire d'une certaine taille (un segment) et l'offset est un deplacement dans ce bloc par rapport a sa base. L'unite de segmentation transforme cette adresse logique en une adresse lineaire sur 32 bits. L'unite de pagination transforme cette adresse lineaire en une adresse physique. Si la pagination n'est pas activee, l'adresse lineaire correspond a l'adresse physique.
Dans un premier temps, nous allons utiliser uniquement le mecanisme de segmentation. Le mecanisme de pagination est plus delicat a mettre en oeuvre. Par ailleurs, il n'est pas necessaire si l'on n'utilise pas de memoire virtuelle.
Le schema ci-dessous illustre le principe de l'adressage en mode protege :

4.3.2 Le mecanisme de segmentation

Les segments sont decrits par des structures de 64 bits appelees descripteurs de segments et stockes dans un tableau appele GDT (Global Descriptor Table). Un descripteur precise l'endroit en memoire ou commence un segment, ou il finit, son type (code, donnees, pile, etc.) ainsi que d'autres informations.

Pour adresser un endroit particulier de la memoire, il faut indiquer dans quel segment on travaille. Le selecteur, directement manipule par le programmeur, peut etre assimile a un pointeur sur un descripteur de segment dans la GDT. Le descripteur donne l'adresse ou commence le segment (la base) et ou il finit (la limite). L'adresse lineaire est obtenue en ajoutant l'offset a la base. Le schema ci-dessous illustre ce mecanisme :

4.3.3 Les descripteurs de segments

Le schema ci-dessous montre la structure d'un descripteur de segment :

La base, sur 32 bits, est l'adresse lineaire ou debute le segment en memoire.
La limite, sur 16 bits, definit la longueur du segment. Si le bit G est a 0, la limite est exprimee en octets, sinon, elle est exprimee en nombre de pages de 4 ko.
Le type definit le type de segment, comme code, donnees ou pile.
Le bit S est mis a 1 pour un descripteur de segment et a 0 pour un descripteur systeme (un genre particulier de descripteur que nous verrons plus tard).
Le DPL indique le niveau de privilege du segment. Le niveau 0 correspond au mode super-utilisateur.
Le bit P est utiliser pour determiner si le segment est present en memoire physique. Il est a 1 si c'est le cas.
Le bit D/B precise la taille des instructions et des donnees manipulees. Il est mis a 1 pour 32 bits.
Le bit AVL est librement disponible.

Les schemas ci-dessous montrent des modeles de descripteurs de segment de code et de segment de donnees :



On remarque que le bit G est mis a 1 (limite exprimee en pages), le code est sur 32 bits (bit D/B a 1), le bit P est a 1 (page presente en memoire), le niveau de privilege est 0 (mode super-utilisateur). Le bit S est mis a 1 pour designer un descripteur de segment et la valeur du type indique que l'on a affaire a un segment de code (voir la documentation Intel pour plus de details).
Pour pouvoir adresser toute la memoire, la base du segment doit etre a 0x0 pour que le segment debute la ou commence la memoire, et sa limite doit etre a 0xFFFFF avec le bit de granularite mis a 1 pour que sa taille soit de 4 Go.

4.4 A quel moment passer en mode protege ?

Il est possible de passer en mode protege a plusieurs moments du boot : a l'execution du programme du secteur de boot ou lors de l'execution du noyau.
Le plus simple est de commuter en mode protege avant que le noyau ne s'execute car en principe, il utilise un jeu d'instructions sur 32 bits, qui ne fonctionne qu'en mode protege. Il est toutefois possible que ce soit le noyau qui effectue la commutation mais cela complique inutilement son ecriture car le code du noyau doit alors etre en partie sur 16 bits et en partie sur 32 bits.
Pour ces raisons, nous allons effectuer la commutation en mode protege au niveau du secteur de boot.

4.5 Un programme de boot qui passe en mode protege

Fichier 'bootsect.asm' :
%define	BASE	0x100	; 0x0100:0x0 = 0x1000
%define KSIZE	1 

[BITS 16]
[ORG 0x0]

jmp start
%include "UTIL.INC"
start:
	mov [bootdrv],dl	; recuparation de l'unite de boot

; initialisation des segments en 0x07C0
	mov ax,0x07C0
	mov ds,ax
	mov es,ax
	mov ax,0x8000	; stack en 0xFFFF
	mov ss,ax
	mov sp, 0xf000

; affiche un msg
	mov si,msgDebut
	call afficher

; charger le noyau
	xor ax,ax
	int 0x13

	push es
	mov ax,BASE
	mov es,ax
	mov bx,0
	mov ah,2
	mov al,KSIZE
	mov ch,0
	mov cl,2
	mov dh,0
	mov dl,[bootdrv]
	int 0x13
	pop es

; initialisation du pointeur sur la GDT
	mov ax,gdtend	; calcule la limite de GDT
	mov bx,gdt
	sub ax,bx
	mov word [gdtptr],ax

	xor eax,eax		; calcule l'adresse lineaire de GDT
	xor ebx,ebx
	mov ax,ds
	mov ecx,eax
	shl ecx,4
	mov bx,gdt
	add ecx,ebx
	mov dword [gdtptr+2],ecx

; passage en modep
	cli
	lgdt [gdtptr]	; charge la gdt
	mov eax,cr0
	or ax,1
	mov cr0,eax		; PE mis a 1 (CR0)

	jmp next
next:
	mov ax,0x10		; segment de donne
	mov ds,ax
	mov fs,ax
	mov gs,ax
	mov es,ax
	mov ss,ax
	mov esp,0x9F000	

	jmp dword 0x8:0x1000    ; reinitialise le segment de code


;--------------------------------------------------------------------
bootdrv: db 0
msgDebut	db	"Chargement du kernel",13,10,0

;--------------------------------------------------------------------
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
gdtend:

;--------------------------------------------------------------------
gdtptr:
	dw	0	; limite
	dd	0	; base


;--------------------------------------------------------------------
;; NOP jusqu'a 510
times 510-($-$$) db 144
dw 0xAA55

Ce programme de secteur de boot ressemble beaucoup a ceux qui ont ete vus precedement.

On stocke le numero de peripherique de boot dans une variable, on initialise les registres relatifs aux segments de code et de donnees puis on affiche un message. Ensuite, on charge le noyau en memoire a l'adresse 0x100.

Avant de commuter en mode protege, il faut initialiser la GDT de facon a ce qu'apres le passage dans le nouveau mode, il n'y ait pas de probleme d'adressage. La GDT doit contenir des descripteurs pour les segments de code, de donnees et de pile. Le code ci-dessous declare et initialise la GDT :

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
gdtend:
L'etiquette 'gdt:' est un pointeur sur le debut du tableau. Chaque descripteur fait 8 octets. Les 8 premiers octets du tableau ne sont pas utilises (je ne sais pas exactement pourquoi mais la documentation des processeurs de type 386 est tres claire, le premier descripteur ne doit pas etre utilise). Le deuxieme descripteur sera celui du segment de code. Une etiquette, 'gdt_cs:', pointe sur ce descripteur afin de rendre le code plus lisible.
Les differents descripteurs sont initialises de facon a ce que les descripteurs de code et de donnees puissent adresser toute la memoire. C'est un modele ou les espaces d'adressage des differents segments sont confondus et recouvrent chacun toute la RAM.
Les schemas ci-dessous montrent les descripteurs de segment de code et de segment de donnees initialises de facon a ce qu'ils adressent toute la memoire :

On remarque que la base de ces segments est a 0 et que leur limite est de 0xFFFFF (c'est un nombre de pages, puisque le bit G est a 1).

Reprenons le deroulement du programme.
On a affiche un message et le noyau est charge en memoire a l'adresse 0x1000. On peut commencer le passage du mode reel vers le mode protege !

On commence par inhiber les interruptions. Cela est necessaire car comme le systeme d'adressage change, les routines appelees par les interruptions ne sont plus valides. Il faudra les reprogrammer. On inhibe les interruptions avec l'instruction 'cli'.

	cli
Les descripteurs de code et de donnees sont definis et le GDT est correctement initialise. Mais il faut ensuite renseigner le processeur pour qu'il prenne en compte la GDT. C'est le role du registre GDTR, qui fait 6 octets, et qui doit etre charge avec la base d'adresse et la limite de la table de GDT.
Dans notre exemple, 'gdtptr' est un pointeur sur une structure qui contient les informations a charger dans le registre GDTR. La structure 'gdtptr' est declaree et initialisee a zero en fin de fichier :
gdtptr:
	dw	0	; limite
	dd	0	; base
Il faut initialiser cette structure avec la base et la limite de la GDT :
	; initialisation du pointeur sur la GDT
	mov ax,gdtend	; calcule la limite de GDT
	mov bx,gdt
	sub ax,bx
	mov word [gdtptr],ax

	xor eax,eax		; calcule l'adresse lineaire de GDT
	xor ebx,ebx
	mov ax,ds
	mov ecx,eax
	shl ecx,4
	mov bx,gdt
	add ecx,ebx
	mov dword [gdtptr+2],ecx

Le registre GDTR est ensuite charge avec l'instruction 'lgdt' :
	lgdt [gdtptr]	; charge la gdt
Une fois le registre GDTR initialisee, on peut passer en mode protege.
On commute en mode protege tres simplement en modifiant le bit 0 du registre CR0 :
; passage en modep
	mov eax,cr0
	or ax,1
	mov cr0,eax		; PE mis a 1 (CR0)
Attention, le processeur vient de commuter en mode protege mais il reste une tache delicate a accomplir : l'initialisation des selecteurs de code et de donnees.
L'instruction qui suit est necessaire pour reinitialiser correctement les buffers du processeur :
	jmp next
next:
Ensuite, on doit reinitialiser les selecteurs des segments de donnees. Les registres DS, ES, FS et GS sont des registres particuliers en ce sens que ce sont les selecteurs par defaut pour les segments de donnees (l'architecture du processeur rend possible l'utilisation de plusieurs segments de donnees mais dans le cas present, nous allons faire pointer ces selecteurs sur le meme segment).
Pour pointer sur un segment de donnees, le selecteur doit pointer sur le descripteur aproprie dans la GDT. On le reinitialise avec l'offset de ce descripteur dans la table. Dans le cas present, cet offset est de 16 octets (0x10 en hexadecimal). On reinitialise donc ces selecteurs avec cette valeur :
	mov ax,0x10		; segment de donne
	mov ds,ax
	mov fs,ax
	mov gs,ax
	mov es,ax
On reinitialise ensuite le registre et le pointeur de la pile :
	mov ss,ax
	mov esp,0x9F000	
L'instruction suivante permet d'executer le code du kernel situe a l'adresse physique '0x1000'. Cette instruction est essentielle car elle permet, outre l'execution du code du noyau, la reinitialisation correcte du selecteur de code sur le bon descripteur (offset 0x8 dans la GDT) et du pointeur d'instructions :
	jmp dword 0x8:0x1000
Voila ! Le programme du secteur de boot est termine. Regardons un peu le code du noyau...

4.6 Un noyau tres simple

4.6.1 Le code

[BITS 32]
[ORG 0x1000]

; Affichage d'un message par ecriture dans la RAM video
mov byte [0xB8000],'H'
mov byte [0xB8001],0x57
mov byte [0xB8002],'E'
mov byte [0xB8003],0x57
mov byte [0xB8004],'L'
mov byte [0xB8005],0x57
mov byte [0xB8006],'L'
mov byte [0xB8007],0x57
mov byte [0xB8008],'O'
mov byte [0xB8009],0x57

end:
	jmp end

Ce noyau affiche un message et boucle ensuite indefiniment. A ce stade, les routines du BIOS permettant d'afficher des caracteres a l'ecran ne sont plus utilisables. Pour afficher un message, le seul moyen que nous avons est d'ecrire nous memes nos propres routines d'affichage.

4.6.2 Afficher quelque chose a l'ecran

La memoire video est mappee en memoire a l'adresse physique 0xB8000. On peut donc afficher des informations en manipulant directement les octets debutant a cette adresse.
La console d'affichage comprend 25 lignes et 80 colonnes. Chaque caractere a l'ecran est decrit par 2 octets en memoire. Un octet contient le code ascii du caractere a afficher et l'octet suivant contient ses attributs (couleur, clignotement...).

Le schema ci-dessous montre comment sont codes les attribus d'un caractere :

Le code suivant affiche le caractere 'H' en blanc sur fond magenta en haut a gauche de l'ecran.

mov byte [0xB8000],'H'
mov byte [0xB8001],0x57
En continuant, on affiche le mot 'HELLO' en haut a gauche de l'ecran. Voici ce qu'on obtient avec 'bochs' :