<-Précédent Retour à l'accueil Contact : etienne"point"sauvage"at"gmail.com Suivant->
  1. Histoire de l'informatique
    1. L'architecture des premiers Personal Computers
    2. Le piège d'IBM
    3. Le mode protégé
  2. Problèmes avec le mode protégé
  3. Solutions en mode protégé
    1. Le GDT (Global Descriptor Table)
    2. Les Descriptors
  4. De la pratique

    Assembleur : passage en mode protégé

    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.

  1. Histoire de l'informatique
  2. 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à.

    1. L'architecture des premiers Personal Computers
    2. 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.

    3. Le piège d'IBM
    4. 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 ?

    5. Le mode protégé
    6. 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é.

  3. Problèmes avec le mode protégé
  4. 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.

  5. Solutions en mode protégé
  6. 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.

    1. Le GDT (Global Descriptor Table)
    2. 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 :

      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.

    3. Les Descriptors
    4. 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 :

      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:

      Source : http://www.c-jump.com/CIS77/ASM/Protection/W77_0090_segment_descriptor_cont.htm

      Nous avons besoin de trois segments :

  7. De la pratique
  8. 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