I. Introduction

Bien que je sois spécialisé dans la programmation sous Delphi et que je programme depuis plusieurs années sous PHP, cet article s'intéresse à un autre langage que j'ai également beaucoup pratiqué : l'assembleur.

Nous allons en effet étudier ici le fonctionnement d'un secteur boot et le passage en mode protégé sur processeur Intel. Rien que ça !

Comme à mon habitude, je ne m'embarrasserai pas de grandes théories, que vous trouverez par ailleurs sur le Net . Nous irons ici droit au but en codant à la main le secteur boot d'une disquette sous FASM. Ce secteur boot sera autosuffisant, aussi serons-nous débarrassés de toutes les notions qui ne touchent pas directement à mon propos (accès disque, système de fichiers, etc.).

II. Mise en place de l'environnement

Pour étudier notre secteur boot, nous aurons besoin d'une machine virtuelle. Aux classiques VMWare ou VirtualBox, j'ai préféré l'émulateur de PC Bochssite officiel de Bochs. Celui-ci propose en effet, dans sa version debug (Bochsdbg.exe), un débogueur intégré qui permet de suivre le processus de boot de la machine depuis sa mise en marche !

Pour compiler le code assembleur, j'utiliserai FASMPage officielle de FASM qui présente un intérêt particulier pour cet article car son fonctionnement par défaut est de produire un fichier binaire « à plat » qui correspond directement au code assembleur.
Voici à titre d'exemple comment produire une image de disquette 1,44 Mo sous FASM :
BOOT00.ASMBOOT00.ASM

BOOT00.ASM, génération d'une disquette 1.44Mb
Sélectionnez

; Disquette 1.44Mb
; 18 Secteurs par piste
; 80 Pistes par face
; 2 faces
 DB 18 * 80 * 2 * 512 DUP(0)

Pour utiliser cette disquette sous Bochs, il faut la déclarer dans la section « Disk & Boot » :
Image non disponibleImage non disponible

Il ne reste plus qu'à cliquer sur Start. Le mode debug se met en route et nous montre la toute première instruction exécutée par le BIOS à l'adresse $F000:$FFF0. Nous n'allons pas nous lancer dans un débogage du BIOS. L'option « c » permet de continuer l'exécution.
Et nous obtenons évidemment une erreur FATAL : No bootable device puisque notre première disquette est non bootable !

Ceci dit, notre environnement est en place. Nous allons pouvoir passer aux choses sérieuses.

III. Disquette bootable

Une disquette est reconnue bootable par le BIOS à partir du moment où les deux derniers octets de son premier secteur ont pour valeur « $AA55 ». Qu'à cela ne tienne, nous allons construire ce secteur boot et effectuer quelques modifications dans notre code.

Première modification, nous laisserons tomber les 2879 secteurs inutiles de notre disquette dont seul le premier est lu par le BIOS ;
Seconde modification, nous allons préparer l'exécution du programme logé dans notre secteur boot.
BOOT01.ASMBOOT01.ASM

BOOT01.ASM, première disquette bootable
Sélectionnez

ORG $7C00                  ; adresse de chargement du secteur boot par le BIOS
 JMP $                     ; boucle éternelle
 DB 510 - ($ - $$) DUP(0)  ; remplir à hauteur de 510 octets
 DW $AA55                  ; et placer le marqueur de disque bootable

Nous y sommes ! le BIOS reconnaît notre disquette de 512 octets, en charge le contenu à l'adresse $7C00 et boucle indéfiniment sur notre instruction JMP !
Pour le vérifier, avant de lancer le processus de boot, ajouter un point d'arrêt à l'adresse $7C00 par la commande "lb 0x7C00".

IV. Message de bienvenue

Notre secteur boot est un peu triste. Nous allons lui demander d'afficher un message. Pour cela, nous ferons appel au BIOS. Après tout, il faut bien qu'il serve à quelque chose.
L'interruption $10 gère la vidéo, la fonction $13 permet d'écrire une chaîne de caractères pointée par ES:BP.
Pour la position du texte à l'écran, nous exploiterons la zone BIOS $0450 qui contient la position actuelle du curseur.
BOOT02.ASMBOOT02.ASM

BOOT02.ASM, première disquette bootable
Sélectionnez

ORG $7C00                  ; adresse de chargement du secteur boot par le BIOS

 XOR AX,AX
 MOV DS,AX
 MOV ES,AX
 MOV BP,msg      ; [ES:BP] => le message
 MOV CX,msg_len  
 MOV AX,$1301    ; Écrire un texte en déplaçant le curseur
 MOV BX,$0070    ; Page 0, attribut noir sur gris
 MOV DX,[$0450]  ; Position Row/Col du curseur
 INT $10

 JMP $                     ; boucle éternelle

msg DB 'BOOT !'
msg_len = $ - msg

 DB 510 - ($ - $$) DUP(0)  ; remplir à hauteur de 510 octets
 DW $AA55                  ; et placer le marqueur de disque bootable

V. Passage en mode protégé

Cette nouvelle version est très excitante ! Nous allons basculer le processeur en mode protégé… et revenir immédiatement en mode réel. Ce n'est pas extraordinaire en soi, mais cet exemple est un prétexte à la mise en place d'un certain nombre d'éléments indispensables à la suite. Notamment, avant de basculer en mode protégé, nous devons désactiver les interruptions sous peine de plantage de la machine.

Afin d'alléger le code source dans les exemples suivants, seule la ligne de commentaire sera indiquée… mais le code doit bien évidemment être présent pour compiler l'exemple !
BOOT03.ASMBOOT03.ASM

BOOT03.ASM, Passage en mode protégé
Sélectionnez

ORG $7C00

; fixer les segments donnée et pile
 XOR AX,AX
 MOV DS,AX
 MOV ES,AX
 MOV SS,AX

; fixer le haut de la pile
 MOV AX,$FFFF
 MOV SP,AX

; Afficher un message via la BIOS
 MOV BP,msg1
 MOV CX,msg1_len
 CALL Print

; masquer les interruptions car notre programme ne DOIT pas être interrompu
 CLI
 IN  AL,$70
 OR  AL,$80
 OUT $70,AL

; basculer en mode protégé
 MOV EAX,CR0
 OR  AL,1
 MOV CR0,EAX

; excitant, non ?

; revenir en mode réel
 MOV EAX,CR0
 AND AL,254
 MOV CR0,EAX

; restaurer les interruptions
 IN  AL,$70
 AND AL,$7F
 OUT $70,AL
 STI

; Second message du BIOS
 MOV BP,msg2
 MOV CX,msg2_len
 CALL Print

; Boucle éternelle
 JMP $

; Afficher le message d'offset ES:BP et de longueur CX
Print:
 MOV AX,$1301   ; Write String, move Cursor
 MOV BX,$0070   ; Page 0, Black on White
 MOV DX,[$0450] ; Row/Col from BIOS Segment 40
 INT $10
RET

; DATA16
 msg1 DB 'BOOT !',13,10
 msg1_len = $ - msg1

 msg2 DB 'BACK !',13,10
 msg2_len = $ - msg2

; Remplissage à hauteur de 510 octets
 DB 510 - ($ - $$) DUP(0)

; Indique au BIOS que c'est un secteur boot
 DW $AA55

VI. Adressage mémoire en mode protégé

Voici notre premier programme en mode protégé. Celui-ci va exploiter la Global Descriptor Table (GDT) pour accéder à la mémoire vidéo en mode protégé. En effet, dans ce mode, la mémoire n'est plus accessible par un couple segment:offset qui donne accès à l'adresse linéaire 16 * segment + offset mais par un couple descripteur:offset. Le descripteur est un offset dans la GDT où est spécifiée, entre autres, une adresse de base.
BOOT04.ASMBOOT04.ASM

BOOT04.ASM - Adressage mémoire en mode protégé
Sélectionnez

; Le BIOS charge le secteur à l'adresse linéaire $7C00
; initialiser les registres de donnée et pile
; fixer le haut de la pile
; Afficher un message via le BIOS
; masquer les interruptions car notre programme ne DOIT pas être interrompu

; en mode protégé, les segments sont remplacés par des descripteurs
; au lieu de calculer une adresse ES:BX par 16 * ES + BX
; on la calcule par GDT[ES].BASE + BX, ES est donc un offset dans la GDT
; GDT : Global Descriptor Table définie par LGDT
 LGDT FWORD [GDT]

; basculer en mode protégé

; Tant qu'on ne touche pas à un registre de segment, il conserve ses attributs
; du mode réel (d'ailleurs le programme continue à fonctionner avec l'ancien CS)

 MOV BX,[CS:$0450]   ; ici on utilise CS (pour l'exemple) qui permet toujours de lire la RAM comme en mode réel
 MOVZX EAX,BH        ; EAX = Ligne
 MOV ECX, 80
 MUL ECX             ; * 80
 AND EBX,$0F         ; EBX = Colonne
 ADD EBX,EAX         ; EBX = 80 * Ligne + Colonne
 SHL EBX,1           ; * 2 (car attribut)

 MOV AX,$08          ; offset dans la GDT
 MOV ES,AX           ; ES est maintenant un descripteur vers la mémoire vidéo (cf GDT_08)
 MOV AX,$7001        ; Caractère Smiley noir sur blanc
 MOV [ES:EBX], AX    ; Affichage du caractère à la position du curseur

; revenir en mode réel

; restaurer le segment ES
 XOR AX,AX
 MOV ES,AX

; restaurer les interruptions
; second message du BIOS
; Boucle éternelle
; DATA16

; Global Descriptor Table

PRESENT    = 10000000b ; l'adresse est valide
USER       = 00010000b ; utilisé par un programme
READ_WRITE = 00000010b ; accessible en écriture (DATA)
                       ; accessible en lecture (CODE)

GDT_00 DQ 0       ; la première entrée est nulle (Descripteur 0 = invalide)

GDT_08 DW 80*25*2 ; Taille    : un écran texte de 80x25 caractère + attribut
       DW $8000   ; Base Low  : adresse physique = $B8000 (segment $B800 * 16)
       DB $0B     ; Base High
       DB PRESENT + USER + READ_WRITE ; Accès     : mémoire présente, utilisateur, accessible en écriture
       DB 0       ; Attributs : aucun (16 bits, taille exprimée en octets)
       DB $00     ; Base Ext  : Base = Base Low + (Base High << 16) + (Base Ext << 24)

GDT DW $ - GDT_00 ; Taille de la GDT
    DW GDT_00     ; Adresse (physique) de la GDT
    DW 0          ; Adresse, partie haute

; Remplissage à hauteur de 510 octets
; Indique au BIOS que c'est un secteur boot

Ce code supporte bien quelques commentaires supplémentaires. La ligne importante dans cet exemple est l'instruction LGDT qui n'a pourtant aucun effet immédiat.
La variable GDT donne deux informations, la taille de notre GDT ($ - GDT_00) et l'adresse de sa première entrée (GDT_00). Notez qu'on parle ici d'adresse physique. Or, notre secteur boot étant chargé dans le segment 0, l'offset GDT_00 est égal à son adresse linéaire.
La première entrée de notre Table de Descripteurs Globale (GDT_00) doit être vide car le descripteur 0 est invalide par convention.
La seconde entrée (GDT_08) définit une zone mémoire de base $B8000 (adresse linéaire de la mémoire vidéo) et de taille 80x25x2 (un écran texte).
Celle-ci est déclarée existante (PRESENT) et accessible en lecture et écriture.

Pour accéder à cette zone mémoire, il faut remplir deux conditions : être en mode protégé et charger l'offset du descripteur ($08) dans un registre de segment. Comme ce n'est pas le cas lors de l'appel à LGDT, l'instruction n'a pas d'effet immédiat.
Par contre, une fois en mode protégé, nous donnons à ES la valeur $08 qui le fait pointer sur l'entrée GDT_08 et la mémoire vidéo.
[ES:$000] est l'attribut du premier caractère de l'écran,
[ES:$001] est le code ascii du premier caractère de l'écran,
[ES:$002] est l'attribut du deuxième caractère de l'écran,
etc.

Notez également que, dans cet exemple, le registre CS n'est jamais modifié. Par conséquent, le programme continue à s'exécuter presque comme en mode réel. Un appel au BIOS planterait la machine car les valeurs des registres de segment ne DOIVENT plus être des segments, mais des offset dans la GDT ; soit on n'y touche pas et ils conservent leur attributs du mode réel, soit on leur donne une valeur valide dans la GDT que nous avons définie.

VII. L'adressage 32 bits

Nous avons vu dans le précédent exemple qu'il ne fallait pas mettre n'importe quoi dans un registre de segment en mode protégé. Dans ce nouveau secteur boot, nous allons basculer CS sur un adressage 32 bits. Cela a un impact direct sur le code assembleur. En effet, en mode réel, l'instruction « MOV EAX,$12345678 » doit être précédée de l'indicateur $66 sous peine d'être interprétée comme un « MOV AX,$5678 ». Quand le registre CS pointe sur un descripteur « 32bits », c'est exactement l'inverse, le préfixe $66 limite l'opération sur EAX par défaut au registre 16 bits « AX ».
Pour cela, nous placerons le code 32 bits à part dans le listing et nous lui appliquerons l'attribut « USE32 » afin que FASM gère correctement ce préfixe.
BOOT05.ASMBOOT05.ASM

BOOT05.ASM, Adressage 32 bits
Sélectionnez

; Le BIOS charge le secteur à l'adresse linéaire $7C00
; initialiser les registres de données et de pile
; fixer le haut de la pile
; Afficher un message via le BIOS
; masquer les interruptions car notre programme ne DOIT pas être interrompu
; GDT : Global Descriptor Table définie par LGDT
; basculer en mode protégé

; faire pointer DS sur 4Go de RAM :)
 MOV AX,$10
 MOV DS,AX  ; nous avons maintenant un sélecteur sur 4 Go de RAM en lecture/écriture

; basculer en adressage 32 bits en donnant à CS la valeur d'un descripteur 32bits
 JMP FAR $08:CODE32 ; $08 => CS = GDT_08

 BACK16: ; adresse de retour en adressage 16 bits, mais toujours en mode protégé

; revenir en mode réel

; CS a une valeur incorrecte pour le mode réel, il a cependant les attributs hérités
; du descripteur $18 et le programme continue à fonctionner. Il faut cependant
; lui rendre sa valeur initiale avant de faire appel au BIOS

; Restaurer les registres de segment
 XOR AX,AX
 MOV DS,AX

 PUSH AX    ; CS = 0 dans notre cas
 PUSH SetCS ; Offset vers  nous allons
 RETF       ; Dépiler CS:IP
SetCS:      ; pour poursuivre avec un CS valide :)

; restaurer les interruptions
; second message du BIOS
; Boucle éternelle

; indiquer à FASM que le code assembleur est ici en adressage 32 bits
USE32
CODE32:
; ici CS est en mode protégé 32 bits
 MOV BX,[$0450]      ; adresse linéaire dans DS
 MOVZX EAX,BH        ; EAX = Ligne
 MOV ECX, 80
 MUL ECX             ; * 80
 AND EBX,$0F         ; EBX = Colonne
 ADD EBX,EAX         ; EBX = 80 * Ligne + Colonne
 SHL EBX,1           ; * 2 (car attribut)

 MOV AX,$7001        ; Caractère Smiley noir sur blanc
 MOV [EBX + $B8000], AX    ; Affichage du caractère à la position du curseur

; retour à un adressage 16 bits
 JMP FAR $18:BACK16 ; $18 => CS = GDT_18
USE16

; DATA16

; Global Descriptor Table

PRESENT    = 10000000b ; l'adresse est valide
USER       = 00010000b ; utilisé par un programme
EXEC       = 00001000b ; exécution autorisée
READ_WRITE = 00000010b ; accessible en écriture (DATA)
                       ; accessible en lecture (CODE)

SIZE_4K    = 10000000b ; indique que la taille est exprimée en pages de 4 ko, sinon ce sont des octets
CODE_32    = 01000000b ; l'adressage se fait en mode 32 bits, sinon en 16 bits (cf préfixe DB 66h)
EXT_SIZE   = 00001111b ; bits qui complètent la taille pour atteindre 4 Go

GDT_00 DQ 0       ; la première entrée est nulle (Descripteur 0 = invalide)

; Descripteur CODE32
GDT_08 DW $FFFF   ; Taille en pages de 4K ($FFFFF * 4K = 4Go)
       DW 0       ; Adresse de base
       DB 0
       DB PRESENT + USER + READ_WRITE + EXEC ; CODE
       DB SIZE_4K + CODE_32 + EXT_SIZE       ; adressage 32 bits (EAX par défaut, préfixe $66 pour AX)
       DB 0

; Descripteur DATA32
GDT_10 DW $FFFF   ; Taille 4 Go
       DW 0       ; Base Low
       DB 0       ; Base High
       DB PRESENT + USER + READ_WRITE        ; DATA
       DB SIZE_4K + CODE_32 + EXT_SIZE       ; adressage 32 bits (utilise ESP et non SP)
       DB 0

; Descripteur CODE16 (pour préparer le retour en mode réel)
GDT_18 DW $FFFF   ; Taille 1 Mo
       DW 0       ; Base Low
       DB 0       ; Base High
       DB PRESENT + USER + READ_WRITE + EXEC ; CODE
       DB EXT_SIZE                           ; adressage 16 bits (AX par défaut, préfixe $66 pour EAX)
       DB 0

GDT DW $ - GDT_00 ; Taille de la GDT
    DW GDT_00     ; Adresse (physique) de la GDT
    DW 0          ; Adresse, partie haute


; Remplissage à hauteur de 510 octets
; Indique au BIOS que c'est un secteur boot

Quelques explication sur cette nouvelle GDT :

  • GDT_08 définit 4 Go de mémoire disponible pour le code exécutable à partir de l'offset 0. L'attribut CODE_32 indique que l'adressage se fait par défaut sur 32 bits ;
  • GDT_10 est utilisé pour le registre DS. Là encore, 4 Go de mémoire en adressage 32 bits (la pile est gérée par ESP et non par SP… mais nous n'utilisons pas SS ici) ;
  • GDT_18 va permettre de basculer le mode protégé dans un adressage 16 bits avant de revenir en mode réel. Ainsi, le registre CS héritera d'attributs compatibles avec le mode réel ;

VIII. Les interruptions en mode protégé

Vous le savez peut-être, les fonctions noyau de Linux sur x86 sont accessibles via l'interruption $80. Dans ce nouveau secteur boot, nous allons ajouter un vecteur d'interruption $01 qui nous montrera comment se définit l'IDT (Interrupt Descriptor Table).
BOOT06.ASMBOOT06.ASM

BOOT06.ASM, Les interruptions
Sélectionnez

; Le BIOS charge le secteur à l'adresse linéaire $7C00
; initialiser les registres de données et pile
; fixer le haut de la pile
; Afficher un message via le BIOS
; masquer les interruptions car notre programme ne DOIT pas être interrompu
; GDT : Global Descriptor Table définie par LGDT

; les vecteurs d'interruptions ne sont plus valides non plus. Il faut utiliser une IDT
; dans cette version, la table ne définit que deux entrées 00 et 01
 LIDT FWORD [IDT]

; basculer en mode protégé
; fixer DS
; basculer en adressage 32 bits en donnant à CS la valeur d'un descripteur 32 bits
; revenir en mode réel
; Restaurer les registres de segment

; restaurer la table d'interruptions du mode réel
 LIDT FWORD [IDT_RM]

; restaurer les interruptions
; Boucle éternelle

; indiquer à FASM que le code assembleur est ici en adressage 32 bits
USE32
CODE32:
 INT $01 ; appel de l'ISR_01

; retour à un adressage 16bits
 JMP FAR $18:BACK16 ; $18 => CS = GDT_18

ISR_00:
IRET

ISR_01:
 MOV BX,[$0450]      ; adresse linéaire dans DS
 MOVZX EAX,BH        ; EAX = Ligne
 MOV ECX, 80
 MUL ECX             ; * 80
 AND EBX,$0F         ; EBX = Colonne
 ADD EBX,EAX         ; EBX = 80 * Ligne + Colonne
 SHL EBX,1           ; * 2 (car attribut)

 MOV AX,$7001        ; Caractère Smiley noir sur blanc
 MOV [EBX + $B8000], AX    ; Affichage du caractère à la position du curseur
IRET

USE16

; DATA16
; Global Descriptor Table

; Vecteur d'interruption 0
IDT_00 DW ISR_00 ; offset_low
       DW $08    ; selector
       DB 0      ; zero
       DB PRESENT + IRQ_32
       DW 0      ; offset_h

; Vecteur d'interruption 1
IDT_01 DW ISR_01
       DW $08
       DB 0
       DB PRESENT + IRQ_32
       DW 0

; Interrupt Descriptor Table
IDT DW $ - IDT_00
    DW IDT_00
    DW 0

; Table d'interruptions pour le mode réel
IDT_RM DW $3FF
       DD 0

; Remplissage à hauteur de 510 octets
; Indique au BIOS que c'est un secteur boot

Nous avons ajouté ici une IDT construite sur un modèle similaire à la GDT.
Elle est chargée par LIDT à laquelle on donne sa taille et son adresse physique.
Par contre, les entrées de la table contiennent, en plus des attributs précisant le mode d'exécution 32 bits, un couple descripteur:offset qui indique le point d'entrée de l'interruption.
L'entrée 0 n'est ici pas utilisée (elle aurait pu), et l'entrée 1 reprend le code 32 bits du précédent exemple.

Notez que dans cet exemple, les interruptions matérielles sont désactivées ! Le choix des numéros d'interruptions est donc (presque) libre. En fait, les interruptions $00 à $1F correspondent aux exceptions du processeur et ne devraient pas être utilisées à d'autres fins.

IX. Les interruptions matérielles

Notre précédent exemple n'était pas mal, mais il faudra bien un jour activer les interruptions matérielles, ne serait-ce que pour lire le clavier au moment où l'on presse une touche.
Pour cela, il va nous falloir une IDT de $22 entrées… soit 272 octets ! cela fait un peu beaucoup pour notre secteur boot de 510 octets !
Ce n'est pas grave. Pour gagner la place qui nous manque, nous allons placer l'IDT en fin de secteur boot et nous l'initialiserons par le code.
BOOT07.ASMBOOT07.ASM

BOOT07.ASM, Les interruptions
Sélectionnez

; Le BIOS charge le secteur à l'adresse linéaire $7C00
ORG $7C00

; initialiser les registres de données et pile
; fixer le haut de la pile
; Afficher un message via le BIOS
; masquer les interruptions car notre programme ne DOIT pas être interrompu
; GDT : Global Descriptor Table définie par LGDT

; les vecteurs d'interruptions ne sont plus valides non plus, il faut utiliser une IDT
 CALL SetupIDT
 LIDT FWORD [IDT]

; Les exceptions et les IRQ possèdent les mêmes adresses. Pour éviter cette confusion,
; nous déplaçons les IRQ vers $20 et $28
 IN AL, $21
 MOV [PICMaster], AL
 IN AL, $A1
 MOV [PICSlave], AL

 MOV BX,$2028
 CALL SetupPIC

; Ne conserver que les IRQ Timer et Keyboard
 MOV AL,$FC
 OUT $21, AL
 MOV AL,$FF
 OUT $A1, AL

; basculer en mode protégé

; donner aux registres suivants des attributs compatibles avec le mode protégé
 MOV AX,$10 ; $10 est un offset dans GDT => DATA32
 MOV DS,AX
 MOV ES,AX
 MOV SS,AX

; basculer en adressage 32 bits en donnant à CS la valeur d'un descripteur 32 bits
 JMP FAR $08:CODE32 ; $08 => CS = GDT_08

 BACK16: ; adresse de retour en adressage 16 bits, mais toujours en mode protégé

; donner aux registres suivants des attributs compatibles avec le mode réel
 MOV AX,$20 ;
 MOV DS,AX
 MOV ES,AX
 MOV SS,AX

; revenir en mode réel
; Restaurer les registres de segment
; restaurer la table d'interruptions du mode réel

; Reconfigurer les interruptions
 MOV BX,$0870
 CALL SetupPIC
 MOV AL,[PICMaster]
 OUT $21, AL
 MOV AL,[PICSlave]
 OUT $A1, AL

; restaurer les interruptions
; Boucle éternelle

SetupPIC:
 MOV AL,$11
 OUT $20,AL ; Init PIC Master
 OUT $A0,AL ; Init PIC Slave
 MOV AL,BH
 OUT $21,AL ; Master IRQ base
 MOV AL,BL
 OUT $A1,AL ; Slave IRQ base
 MOV AL,4
 OUT $21,AL ; Continue Init Master
 MOV AL,2
 OUT $A1,AL ; Continue Init Slave
 MOV AL,1
 OUT $21,AL
 OUT $A1,AL
RET

SetupIDT:
  MOV DI,IDT_00
  MOV CX,$20
 NextIDT:
  MOV AX,ISR_00
  STOSW
  MOV AX,$08
  STOSW
  MOV AX,256 * (PRESENT + IRQ_32)
  STOSW
  XOR AX,AX
  STOSW
  LOOP NextIDT
  MOV AX,ISR_20
  STOSW
  MOV AX,$08
  STOSW
  MOV AX,256 * (PRESENT + IRQ_32)
  STOSW
  XOR AX,AX
  STOSW
  MOV AX,ISR_21
  STOSW
  MOV AX,$08
  STOSW
  MOV AX,256 * (PRESENT + IRQ_32)
  STOSW
  XOR AX,AX
  STOSW
RET

; indiquer à FASM que le code assembleur est ici en adressage 32 bits
USE32
CODE32:
; ici CS est valide en mode protégé 32 bits
; nous pouvons réactiver les interruptions
 IN  AL,$70
 AND AL,$7F
 OUT $70,AL
 STI

; On attend une touche
WaitKey:
 MOV AL,[ScanCode]
 CMP AL, 0
 JZ WaitKey

; désactiver les interruptions
 CLI
 IN  AL,$70
 OR  AL,$80
 OUT $70,AL

; retour à un adressage 16 bits
 JMP FAR $18:BACK16 ; $18 => CS = GDT_18

ISR_00:
IRET

ISR_20: ; Interruption Timer
 PUSH DS
 PUSH ES
 PUSHAD

 MOV AX,$10
 MOV DS,AX

 MOV AL,[$B8000 + 2 * 24 * 80 + 1]
 INC AL
 MOV [$B8000 + 2 * 24 * 80 + 1],AL

ISR_Done:
 MOV AL, $20
 OUT $20,AL
 POPAD
 POP ES
 POP DS
IRET

ISR_21: ; Interruption Clavier
 PUSH DS
 PUSH ES
 PUSHAD

 MOV AX,$10
 MOV DS,AX

 IN AL,$60
 MOV [ScanCode], AL

JMP ISR_Done

USE16

; DATA16
 msg1 DB 'BOOT !',13,10,'- press any key -',13,10
 msg1_len = $ - msg1

 msg2 DB 10,'BACK !',13,10
 msg2_len = $ - msg2

 PICMaster DB 0
 PICSlave  DB 0

 ScanCode  DB 0

; Global Descriptor Table

PRESENT    = 10000000b ; l'adresse est valide
USER       = 00010000b ; utilisé par un programme
EXEC       = 00001000b ; exécution autorisée
IRQ_32     = 00001110b ; Interrupt Gate 32 bits
READ_WRITE = 00000010b ; accessible en écriture (DATA)
                       ; accessible en lecture (CODE)

SIZE_4K    = 10000000b ; indique que la taille est exprimée en pages de 4 ko, sinon ce sont des octets
CODE_32    = 01000000b ; l'adressage se fait en mode 32 bits, sinon en 16 bits (cf préfixe DB 66h)
EXT_SIZE   = 00001111b ; bits qui complètent la taille pour atteindre 4 Go


GDT_00 DQ 0       ; la première entrée est nulle (Descripteur 0 = invalide)

; Descripteur CODE32
GDT_08 DW $FFFF   ; Taille en pages de 4 ko ($FFFFF * 4K = 4Go)
       DW 0       ; Adresse de base
       DB 0
       DB PRESENT + USER + READ_WRITE + EXEC ; CODE
       DB SIZE_4K + CODE_32 + EXT_SIZE       ; adressage 32 bits (EAX par défaut, préfixe $66 pour AX)
       DB 0

; Descripteur DATA32
GDT_10 DW $FFFF   ; Taille 4 Go
       DW 0       ; Base Low
       DB 0       ; Base High
       DB PRESENT + USER + READ_WRITE        ; DATA
       DB SIZE_4K + CODE_32 + EXT_SIZE       ; adressage 32 bits (utilise ESP et non SP)
       DB 0

; Descripteur CODE16 (pour préparer le retour en mode réel)
GDT_18 DW $FFFF   ; Taille $FFFFF = 1 Mo
       DW 0       ; Base Low
       DB 0       ; Base High
       DB PRESENT + USER + READ_WRITE + EXEC ; CODE
       DB EXT_SIZE
       DB 0

; Decripteur DATA16 (pour préparer le retour en mode réel)
GDT_20 DW $FFFF
       DW 0
       DB 0
       DB PRESENT + USER + READ_WRITE
       DB EXT_SIZE
       DB 0

GDT DW $ - GDT_00 ; Taille de la GDT
    DW GDT_00     ; Adresse (physique) de la GDT
    DW 0          ; Adresse, partie haute

; Interrupt Descriptor Table
IDT DW $22 * 8 ; $ - IDT_00
    DW IDT_00
    DW 0

; Table d'interruptions pour le mode réel
IDT_RM DW $3FF
       DD 0

; Remplissage à hauteur de 510 octets
; Indique au BIOS que c'est un secteur boot

; Nous n'avons plus assez de place dans nos 510 octets de secteur boot pour notre code
; Pour dégager un peu d'espace, nous plaçons l'IDT après la fin du secteur boot et
; nous l'initialiserons par le code (SetupIDT)
IDT_00:

Ouf ! tout cela tient dans un secteur boot.
Plusieurs notions sont abordées ici :le PIC (qui gère les interruptions) doit être reprogrammé pour déclencher des interruptions qui ne chevauchent pas les exceptions du processeur (SetupPIC). De ce fait, l'interruption Timer se retrouve en $20 et le clavier en $21.
Un descripteur DATA16 a été ajouté pour restaurer les attributs 16bits de DS, ES et surtout SS.
Finalement, l'IDT est placée au delà des 512 octets du secteur boot. Les $1F premières entrées pointent sur ISR_00 qui ne fait rien. L'IRQ $20 gère le timer et l'IRQ 21 le clavier.
Grâce à cela, nous pouvons réactiver les interruptions en mode protégé et attendre qu'une touche soit pressée pour revenir en mode réel.

X. Conclusion

Nous voici au bout de ce premier tutoriel sur le mode protégé et les secteurs boot. J'aurais pu pousser un peu plus loin les explications avec un exemple de changement de tâche (TSS) mais un simple secteur boot est un peu petit pour cela, le gain de place nécessaire rend l'initialisation de tout cela un peu confuse.
La preuve : BOOT08.ASMBOOT08.ASM.
Si tout va bien, dans un prochain article, je vous expliquerai comment réaliser cela en chargeant la suite du code depuis le secteur suivant de la disquette pour un programme de 1024 octets !

XI. Remerciements

J'ai cherché beaucoup d'informations sur le Net pour comprendre et rédiger cet article. Mais au final, ce sont les PMode Tutorials in C & ASM (c) 2000 by Alexei A. FrounzeMTuts qui ont été les sources d'inspiration principales de cet article.

Un grand merci également à Obsidian qui a pris le temps nécessaire pour corriger mes multiples boulettes orthographiques et grammaticales.