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.
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 :
Les schemas ci-dessous montrent des modeles de descripteurs de segment
de code et de segment de donnees :
%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.
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'.
cliLes 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.
gdtptr: dw 0 ; limite dd 0 ; baseIl 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],ecxLe registre GDTR est ensuite charge avec l'instruction 'lgdt' :
lgdt [gdtptr] ; charge la gdtUne fois le registre GDTR initialisee, on peut passer en mode protege.
; 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.
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).
mov ax,0x10 ; segment de donne mov ds,ax mov fs,ax mov gs,ax mov es,axOn reinitialise ensuite le registre et le pointeur de la pile :
mov ss,ax mov esp,0x9F000L'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:0x1000Voila ! Le programme du secteur de boot est termine. Regardons un peu le code du noyau...
[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.
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],0x57En continuant, on affiche le mot 'HELLO' en haut a gauche de l'ecran. Voici ce qu'on obtient avec 'bochs' :