Assembleur Intel avec NASM


précédentsommairesuivant

VIII. Assembleur : passage en mode protégé

49 commentaires Donner une note à l'article (5)

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 Go 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 :

 
Sélectionnez

				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) :

 
Sélectionnez

					 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.

 
Sélectionnez

?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  2, le  1, on est dedans, et le  0 n'existe pas)
	xor 	dh, 	dh 		; Tête de lecture  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
	

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2011 Etienne Sauvage. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.