Assembleur Intel avec NASM en mode protégé

Etienne Sauvage

À la découverte de l'assembleur, la vengeance. Suite de ceci.

Commentez

- Titre : Assembleur Intel avec NASM en mode protégé

- Auteur : Etienne Sauvage

- Parution : 21 avril 2011

- Licence : 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.

Sommaire

- I. Un contrôleur clavier

- I-1. Que se passe-t-il lors de l'appui sur une touche ?

- I-2. Comment ça se passe ?

- I-3. Convertir en caractères

- I-4. Lecture au clavier

- I-5. Du code

- II. VESA en mode protégé

- II-1. Des émulateurs

- II-2. Du Linear Frame Buffer

- II-3. Du dessin

- II-4. De Bresenham

- II-4-a. L'idée

- II-4-b. Le comment

- II-4-c. La fonction

- II-5. De La Bruyère

- II-6. Du neuf

- II-7. Du code

- III. Une carte de la mémoire

- III-1. De la mémoire vive

- III-2. De la taille

- III-3. Des périphériques

- III-4. De la cartographie

- III-5. De la pratique

- III-6. De la bonne exécution de nos basses oeuvres

- III-7. Du code

- IV. Remerciements

I - Un contrôleur clavier

Commentez

Relu par ClaudeLELOUP.

Il n'y a pas à dire, le mode protégé, c'est violent. Au chapitre précédent, on a retrouvé les interruptions. Lesquelles malheureusement sont vides. Retrouver l'entrée clavier, ce ne serait pas mal.

I-1 - Que se passe-t-il lors de l'appui sur une touche ?

Lors de l'appui sur une touche, le PIC déclenche l'interruption à laquelle est branchée l'IRQ1. Doit s'ensuivre une discussion protocolaire avec le processeur afin de transmettre au processeur les connaissances du PIC.

I-2 - Comment ça se passe ?

Pour faire ça convenablement, nous allons écrire l'interruption déclenchée par l'IRQ1. Pour écrire une interruption, on ne se soucie pas du contexte, il est sauvegardé par l'instruction INT. On va commencer par interdire les interruptions : cli. Ensuite, on demande au PIC s'il est disponible pour un échange fructueux :

.attendBuffer:
in        al,        0x64    ; lire statut
test    byte al,1        ; bus de sortie prêt ?
jz        .attendBuffer    ; non => boucler

Ce qui signifie qu'on attend sagement qu'il y ait un octet avec le bit de poids faible à 1 sur le port 0x64, signifiant qu'il y a une entrée clavier à lire. Une fois qu'on est sûr qu'il y a une touche à traiter, on peut magnanimement en prendre connaissance :

in        al,     0x60    ;Lit la touche pressée
cmp        al,     224        ;224 est un code spécial, ça veut dire qu'il y a autre chose derrière, mais pour l'instant on s'en fiche
je        .attendBuffer    ;On va donc lire la suite

Le caractère se trouve sur un autre port, le port 0x60. Ensuite, le traitement commence, et pour cela, il nous faut un peu d'histoire. Le circuit qui gère le clavier n'est pas tout jeune. Il est standard, éprouvé, et vient de gens dont la principale motivation n'était pas le rendu typographique. De base, le clavier envoyait un octet qui donnait à la fois la touche concernée et son état : pressée ou relâchée. Cela fait deux codes par touche, sur un octet. On peut donc avoir 128 touches différentes. Il faut aussi savoir qu'au niveau où nous sommes, on parle de touche et non de caractère : la touche 23 a été relâchée, que celle-ci soit un 'A', un 'é' ou F4. Il a été remarqué que ce 128 pouvait être une limite un peu basse. Le code de touche a été étendu - voilà pourquoi on peut encore entendre qu'un ordinateur actuel possède un clavier étendu - sur deux octets. Pour marquer l'utilisation de code étendu, le premier octet prend des valeurs spéciales, dont 224, celle testée ici. Les codes clavier étendu ont été si bien faits que quand on ne prend pas en compte le mode étendu, le code signifie quand même quelque chose, caractéristique que nous mettons à profit ici en "oubliant" le code 224.

Pour la suite, j'ai un peu joué quand même. Après tout, on a bien le droit de se faire plaisir. Je me suis dit que j'allais stocker une carte de toutes les touches appuyées. Comme ça, si vous voulez savoir à n'importe quel moment si une touche est appuyée, un coup d'œil sur cette carte et vous saurez si Shift ou Ctrl sont pressées. Mais si vous voulez faire "A + 3 + ,", c'est aussi possible. Pour ce faire, on a besoin de stocker 128 bits, un bit (pressée : oui ou non) par touche gérée par ce qu'il faut bien appeler un gestionnaire de clavier. 128 bits, c'est 16 octets: touchesActives: dd 0, 0, 0, 0. La prochaine étape consiste donc à mettre le bit de la touche traitée à la valeur convenable. Première étape, trouver l'octet concerné dans la table :

mov bl, al
shr bl, 3; On divise le code par 8 pour savoir dans quel octet il se situe
push eax    ; On sauve le caractère dans la pile
mov cl, 8
div cl        ; On le divise par 8
mov cl, ah    ; Pour une division par 8 bits, le reste est dans AH
pop eax
mov edx, 1
shl dl, cl
add ebx, touchesActives ;C'est l'adresse de l'octet contenant ce qui nous intéresse
cmp        al,        128        ;Les touches appuyées ont un code inférieur à 128, les touches relâchées, c'est le même code qu'appuyées plus 128
jb        .stockeActif
sub        al,        128
sub        ebx,    128/8
not dl
and byte[ebx], dl
jmp .suite
.stockeActif:
or byte[ebx], dl    ; Un OU met le bit à 1, un NOT AND le mettra à 0
mov [derniereTouche], al

À noter dans ce morceau de code : l'utilisation de la position du reste d'une division en fonction de la taille des opérandes, chose à laquelle il faut toujours faire attention. SHL et sa copine SHR ne peuvent décaler que d'un nombre constant ou du contenu du registre C. La différence de traitement entre touche pressée/touche relâchée est faite en un seul endroit, ce qui donne un code un peu contre-intuitif (pourquoi cette soustraction de 128/8 sur ebx ?). Notez l'utilisation du AND pour mettre des bits à 0 et du OR pour les mettre à 1. On termine, en cas de touche pressée, par la stocker en mémoire, afin que d'autres sous-programmes puissent y accéder. C'est grossier, vu qu'on stocke aussi les modificateurs, et je suis quasiment certain qu'un bon gestionnaire de clavier utilise un buffer circulaire.

Il reste à accuser réception, remettre les interruptions en place et quitter (ici, en appelant l'interruption PIC qui ne fait rien) :

.suite:
in         al,     0x61
or        al,     130
out     0x61,     al        ;Dit à l'ordi qu'une touche a été pressée

and     al,        127
out     0x61,    al        ;Reinitialisation terminée

sti
jmp     interruptionPICMaitreParDefaut

I-3 - Convertir en caractères

Car, suite à l'étape précédente, nous avons bien la dernière touche tapée, mais nous n'avons aucune idée du caractère auquel elle correspond. Nous allons avoir besoin d'une table de conversion qui fera la correspondance entre numéro de touche et caractère sérigraphié sur la touche. Voici cette table :

traductionASCII: db '.','.','1','2','3','4','5','6','7','8','9','0','.','+','.','.','a','z','e','r','t','y','u','i','o','p','^','$',13,'.','q','s','d','f','g','h','j','k','l','m','%','.','.','*','w','x','c','v','b','n',',',';',':','!';54 caractères gérés

La position du caractère dans cette table est son numéro de touche.

I-4 - Lecture au clavier

Vous vous souvenez de la routine de lecture de caractères ? Oui, nous avons progressé, depuis, mais nous ne l'avons plus depuis le passage en mode protégé. Il faut la refaire.

L'idée est de commencer par vider le similitampon que nous avons dans la routine d'interruption. Ensuite, nous lirons le contenu de ce tampon. L'octet ainsi récupéré nous donne l'index à lire dans le tableau de conversion, et nous avons ainsi notre caractère.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; litChaine                                                        ;;
;;------------------------------------------------------------------;;
;; Ecrit dans la chaîne pointée par EDI les caractères écrits au     ;;
;; clavier, jusqu'à un retour chariot.                                 ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
litChaine:
push eax
push ebx
xor eax, eax
mov byte [derniereTouche], 0
mov esi, edi
.attend_clavier:
cmp byte [derniereTouche], 0
jz .attend_clavier
mov al, [derniereTouche]
mov byte [derniereTouche], 0

mov ebx, traductionASCII
add ebx, eax
mov al, [ebx]
stosb
cmp al, 13
je .fin_attend_clavier
mov al, 0
stosb
call afficheChaine
dec edi
dec esi
jmp .attend_clavier
.fin_attend_clavier:
pop ebx
pop eax
ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin litChaine                                                    ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

I-5 - Du code

L'ensemble du code se trouve ici : Paquet

II - VESA en mode protégé

Commentez

Relu par ClaudeLELOUP.

Oui, chers amis, vous avez bien lu : enfin de vrais graphismes en mode protégé ! Adieu mode VGA, SVGA, Hercules et autres reliquats des années 1990 !

II-1 - Des émulateurs

Si vous m'avez suivi jusqu'ici, vous devez savoir que nous travaillons sur émulateur. Si jusqu'à présent, n'importe quel émulateur pouvait vaguement convenir à quelques exceptions près, ce n'est plus le cas. Warum ? Weil komssa ist. Traduction : pourquoi ? Parce que visiblement maints émulateurs ont bien des difficultés à reproduire correctement le VESA 2.0. Oui, je sais, l'allemand est une langue magnifique.

Alors, bon, voici les émulateurs que j'ai testés, si vous avez d'autres retours, dites-le-moi.

- Bochs : je ne me permettrai pas de critiquer vu que plein de gens en sont très contents, mais je n'ai même pas réussi à avoir un vague VESA 1.2.

- VMWare : le VESA 2.0 est spécifiquement écrit dans l'OEM, mais soit je suis nul, soit c'est une mauvaise blague. Sur un paquet de 80 ko (kilo-octet), les derniers 40 ko pointent sur les mêmes pixels que les premiers 40 ko, ce qui donne des fascés.

- VirtualBox : ça marche. Enfin, à peu près. 'Faut apparemment pas être trop rapide, notamment pour un remplissage standard d'écran. Sinon, leur VESA 2.0 a un comportement de VESA 2.0.

Donc la suite se déroulera sur VirtualBox.

II-2 - Du Linear Frame Buffer

Qu'on sigle LFB. C'est un champ donné à l'octet 40 dans la structure renvoyée par l'appel BIOS 0x10, fonction 0x4F01, quand on demande des informations sur un mode vidéo particulier. Il s'agit d'une adresse sur 32 bits, à partir de laquelle on trouvera la valeur de chaque pixel de l'écran.

Et vous savez quoi ? Pour éviter des additions fastidieuses et nous accélérer le tempo, nous allons déclarer un segment de données qui commence à cette adresse. Après tout, on a appris à jouer avec la GDT, oui ou non ?

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
gdt_flb:
dw 0xFFFF, 0x0000
db 0x00, 10010011b, 11011111b, 0xE0
gdtend:

pointeurGDT:
dw    gdtend-gdt                ; taille
dd    (BOOT_SEG << 4) + gdt    ; base

C'est initialisé à 0x00E00000, je crois. C'est parce que VirtualBox place son écran à cet endroit-là. De toute façon, on peut mettre n'importe quoi, ce sera initialisé proprement à la lecture. On ne met pas de limite supérieure au segment, parce que je n'en ai ni envie ni besoin pour l'instant. Le mélange rock'n roll de mots et d'octets n'a pour objectif que de me faciliter la création à la main.

On remplit comme ça :

mov ax, [FlatMemory]; bits de poids faible
mov [gdt_flb + 2], ax
mov ax, [FlatMemory + 2]; bits de poids fort
mov [gdt_flb + 4], al
mov [gdt_flb + 7], ah

On fait un usage intensif de la structure d'un descripteur de segment, donnée au chapitre 8. Ah, oui, c'est vrai : je fais ici, dans le chargeur d'amorçage, référence à FlatMemory, mémoire aplatie. Cette valeur est une adresse codée en dur par un %define :

;Définitions de données qui seront chargées par le BIOS
;Informations du pilote VESA
%define VESASignature 512;'VESA', signature de 4 octets
%define VideoModePtr VESASignature + 14;: dd 0; Pointeur vers les modes accessibles

;Informations d'un mode vidéo
%define ModeAttributes VESASignature + 256; Attributs du mode mot
%define BytesPerScanLine ModeAttributes + 16;: dw 0; Octets par "scanline" mot
%define XResolution ModeAttributes + 18;: dw 0; Résolution horizontale
%define YResolution ModeAttributes + 20;: dw 0; Résolution verticale
%define BitsPerPixel ModeAttributes + 25;: db 0; Bits par pixel
%define FlatMemory ModeAttributes + 40

Parce que, si on ne mettait pas cette adresse en dur, l'ensemble de ces deux jeux de données ferait 512 octets, soit précisément la taille de notre secteur d'amorçage, et nous n'aurions donc pas de place pour du code. Plutôt que de faire des chargements de secteur, de vérifier qu'on a bien ce qu'on veut là où on le veut, je sais qu'à la fin du segment (on est en 16 bits !), j'ai de l'espace, vu que je n'ai dedans que 512 octets, je balance ça à une adresse fixée à cet endroit.

L'adresse du LFB oblige à se poser la question suivante : que deviennent les deux autres fenêtres, les célèbres fenêtres A et B qu'on utilise en mode réel ? Elles existent toujours et sont définies, mais à première vue, leur utilisation est incompatible avec celle du LFB. Et de fait, les deux utilisations sont incompatibles. Pour utiliser le LFB, il faut, lors de l'appel au changement de mode, passer le bit n° 14 à 1, chose que je fais ainsi :

mov ax, 0x4F02
mov bx, [mode_souhaite]
or bx, 0100000000000000b ; Le bit n°14 à 1 demande l'utilisation de la mémoire linéaire.
int 0x10 ; Changement de mode vidéo

Détail pendant que j'y pense : le binaire peut aussi se suffixer par 'b'.

II-3 - Du dessin

Allez, jeune France, on y retourne ! Je ne donne pas la fonction de nettoyage d'écran vu qu'elle ne fonctionne pas bien, mais si quelqu'un se sent l'âme d'un peintre en bâtiment, je suis preneur. Comme on n'a pas d'écran de 65536 par 65536, on peut utiliser un registre de 32 bits pour coder à la fois l'abscisse et l'ordonnée. L'abscisse sera ici le mot de poids fort et l'ordonnée le mot de poids faible. On met EDI à 0, le sélecteur de segment ES sur le segment n° 3 (vérifiez la table) avant l'appel, deux multiplications et une somme, on écrit et c'est fini.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; metPixel                                                            ;;
;;------------------------------------------------------------------;;
;; Colorie le pixel dont la position est dans EAX (mot haut : X,    ;;
;; mot bas : Y) avec la couleur contenue dans couleurPoint            ;;
;; ES doit contenir le descripteur de segment de l'écran.            ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
metPixel:
push edi
push ecx
push eax
push ebx
push edx

mov ebx, eax
and eax, 0xFFFF
shr ebx, 16
xor ecx, ecx
mov cx, [XResolution]
mul ecx                    ; On fait une multiplication 32 bits à partir de valeurs de 16 bits.
add eax, ebx
xor ch, ch
mov cl, [octetsParPixel]
mul ecx                    ; On fait une multiplication 32 bits à partir de valeurs de 8 bits.
mov edi, eax
mov eax, [couleurPoint]
stosd

.retour:
pop edx
pop ebx
pop eax
pop ecx
pop edi
ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin metPixel                                                        ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

J'espère que vous avez vu l'imbécillité que j'ai faite, je la laisse pour l'édification des masses.

Allez, entre les push et les pop, y'a pas beaucoup de code, mes enfants !

Mettre la couleur, c'est obligé. Ce n'est pas ça.

Calculer la coordonnée, obligatoire aussi.

Il ne reste que la bourde.

Oui, le nombre d'octets par pixel. On écrit 32 bits de couleur, le nombre d'octets par pixel est donc nécessairement de 4. Ce qui implique que dans le code d'amorçage, on ne sélectionne que les modes à 32 bits. Autre subtilité : comme on fait une multiplication, le registre EDX est modifié. Il faut donc le mettre sur la pile au début de la fonction et le restaurer à la fin. Pensez-y, on peut perdre des heures bêtement.

II-4 - De Bresenham

Je vous en avais déjà parlé il y a bien longtemps, voici le retour de l'algorithme de Bresenham, l'étoile des dessinateurs de segment de droite. Sauf qu'à l'époque, il s'agissait d'une interprétation quelque peu cavalière. Nous allons faire mieux, de surcroît en plus court.

II-4-a - L'idée

On se propose de tracer un segment de droite pixel par pixel. Pour ce faire, nous allons débuter à une extrémité du segment. Nous allons nous intéresser aux segments dont la direction est comprise entre Est et Sud-Est. L'idée de base, c'est d'avancer en abscisse de 1 pixel à chaque pas. Deux possibilités : soit on reste sur la même ligne (on va plein Est pour cette itération), soit on passe à la ligne du dessous (on va au Sud-Est). Ça, c'est pour le cas où on dessine de gauche à droite et de haut en bas, avec un écart en abscisse supérieur à l'écart en ordonnée. Ce qui fait qu'il est valable pour un huitième de cercle. Il suffit de changer le sens de parcours et d'inverser abscisse et ordonnée pour aller où l'on veut.

II-4-b - Le comment

- On se fixe une variable particulière : l'écart à la droite optimale.

- On initialise cet écart à la longueur à parcourir en abscisse. C'est la valeur maximale que peut prendre cet écart : le segment de droite orienté Sud-Est.

- On dessine le point où l'on est.

- On ajoute 1 à son abscisse. On va regarder si on dessine sur la même ligne ou sur la ligne d'en dessous.

- On ôte à l'écart la longueur à parcourir en ordonnée. Cela donne l'erreur que l'on commet.

- Si l'écart est inférieur à la longueur à parcourir en ordonnée, c'est qu'on est trop haut. On ajoute 1 à l'ordonnée du point sur lequel on est. On ajoute à l'écart la longueur à parcourir en ordonnée.

- Et on recommence.

II-4-c - La fonction

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; afficheLigne                                                        ;;
;;------------------------------------------------------------------;;
;; Dessine un segment de droite entre le point stocké dans EAX et     ;;
;; celui stocké dans EBX (mot haut : X,    mot bas : Y) avec la         ;;
;; couleur contenue dans couleurPoint                                ;;
;; ES doit contenir le descripteur de segment de l'écran.            ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Y2: dd 0
afficheLigne:
push esi
push eax
push ebx
push ecx
push edx
push edi

cmp     eax, ebx    ;On travaille dans le sens des abscisses positives
jbe .ordreOK
xchg     eax, ebx    ;On inverse si ce n'est pas le cas
.ordreOK:
mov     [Y2], ebx    ;On stocke le point d'arrivée
sub     ebx, eax    ;On calcule les longueurs à parcourir en X et Y
mov     cx, bx        ;deltaY
shr     ebx, 16        ;deltaX
or         bx, bx        ;Si deltaX est nul, on sera sur une ligne verticale
jz .ligneVerticale
or        cx, cx        ;Si deltaY est nul, on sera sur une ligne horizontale
jnz .test_deltaY
mov     cx, bx        ;CX contient le nombre de pixels à afficher
mov     ebx, 0x10000;Incrément de 1 pixel vers la droite
jmp .avance_H_V
.ligneVerticale:
mov     ebx, 0x00001;Incrément de 1 pixel vers le bas
.avance_H_V:
call metPixel        ;Affichage du pixel
add eax, ebx        ;Incrément
loop .avance_H_V
.fin_afficheLigne:
call metPixel        ;Affichage du dernier pixel
pop edi
pop edx
pop ecx
pop ebx
pop eax
pop esi
ret

.test_deltaY:
push 0x10000        ;Incrément de 1 pixel vers la droite
push 1                ;Incrément de 1 pixel vers le bas
jns .suite            ;Si deltaY est positif, on est standard
neg cx                ;Sinon, on l'oppose, ainsi que l'incrément en Y
neg dword[esp]
.suite:
cmp bx, cx            ;Si deltaY>deltaX
jae .principal
xchg bx, cx            ;On échange deltaY et deltaX
mov edx, [esp + 4]    ;Ainsi que les incréments
xchg edx, [esp]
mov [esp + 4], edx
.principal:                ;Et on continue normalement
mov dx, bx            ;DX contient l'écart
.boucle:
call metPixel
sub dx, cx            ;On enlève deltaY
cmp dx, cx            ;Si on est inférieur ou égal à deltaY
ja .affichage
add eax, [esp]        ;On applique l'incrément en Y
add dx, bx            ;On ajoute deltaX à l'écart
.affichage:
add eax, [esp + 4]    ;On applique l'incrément en X
cmp eax, [Y2]        ;Sommes-nous arrivés ?
jb .boucle
pop edx                ;On s'en va proprement
pop edx
jmp .fin_afficheLigne
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin afficheLigne                                                    ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

C'est quand même sacrément plus propre que la version précédente. Le 32 bits permet quand même un joli coup de rabot en taille de code, ça fait plaisir. Et en général, un code plus court est un code plus rapide. Là, on pourrait calculer directement l'adresse mémoire pour faire mieux. S'il y a des amateurs, encore une fois, c'est avec plaisir. C'est assez simple à faire.

II-5 - De La Bruyère

La fonction d'affichage des caractères a eu aussi droit à un coup de lifting.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; afficheCaractere                                                    ;;
;;------------------------------------------------------------------;;
;; Dessine un caractère au point stocké dans EAX (mot haut : X,        ;;
;; mot bas : Y) avec la couleur contenue dans couleurPoint.            ;;
;; ES doit contenir le descripteur de segment de l'écran.            ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
afficheCaractere:
push edx
push esi
push ecx
push eax
push ebx

mov ebx, eax        ;EBX stocke le pixel courant
mov eax, ecx        ;ECX contient le n° du caractère
mov ecx, 6            ;6 lignes par caractère
mul ecx
mov esi, Alphabet    ; adresse de la lettre
add esi, eax        ; si contient l'adresse de la lettre
.colonne:
push ecx
mov dl, 0b10000000
mov ecx, 8; On affiche 8 colonnes
lodsb
.ligne:
test al, dl
jz .suite
xchg eax, ebx
call metPixel
xchg eax, ebx
.suite:
shr dl, 1
add ebx, 0x10000
loop .ligne
pop ecx
inc ebx; passage à la ligne suivante
sub ebx, 8*0x10000
loop .colonne

pop ebx
pop eax
pop ecx
pop esi
pop edx
ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin afficheCaractere                                                ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Ici aussi, on peut faire la même optimisation que précédemment. Si une âme charitable s'en sent le courage...

II-6 - Du neuf

Il faut toujours faire du neuf, même pas beaucoup, pour dire. Là, ce sera une première approche d'édition de texte avec nos fonctions. On tape au clavier, on appuie sur Entrée, hop ça s'affiche.

II-7 - Du code

L'ensemble du code se trouve ici : Paquet

III - Une carte de la mémoire

Commentez

Relu par ClaudeLELOUP.

Il faut maintenant dessiller nos yeux : nous avons un nano-système d'exploitation. Quoique "nano" ne signifie pas "naze", il faut reconnaître qu'il ne remplit pas vraiment les services qu'on en attend. Néanmoins, il permet de taper quelques caractères, et donc d'exploiter un ordinateur : c'est un système d'exploitation. Normalement, à cet instant, la poitrine se gonfle d'orgueil et on court ajouter "développeur système" sur son CV. N'allons pas jusque-là, mais ne boudons pas notre plaisir en nommant notre système d'exploitation. Je propose KarlOS, calembour source de calembours. Car l'OS ainsi nommé représente l'exploitation de l'ordinateur par l'ordinateur, principe cher à Karl, et est vraiment fait pour la rigolade, principe tout aussi cher à Carlos. Et l'emprunt à la langue de Shakespeare me sera pardonné par les rigolos.

Mais trêve de plaisanterie. Notre mission, aujourd'hui, consistera à préparer le terrain pour la gestion de la mémoire.

III-1 - De la mémoire vive

Que dis-je, de la mémoire vive ? Des forces vives de la nation, plutôt !

La mémoire vive, on l'a utilisée, je suis persuadé que si vous en êtes là, vous voyez de quoi je cause, Windows vous a certainement déjà insulté en vous disant qu'il n'en avait pas assez (c'est un junkie, la mémoire vive étant au système d'exploitation ce que l'héroïne est à la littérature policière, admirez le double sens au passage, merci), voire vous en trafiquez pour les plus atteints. Mais un petit rappel ne ferait pas de mal, je suis sûr.

La mémoire vive, c'est votre mémoire à vous dans le sens commun : le souvenir des vacances à la mer, les tartes de mémé, Ragondin le vieux chien de la famille dont l'arbre généalogique fait croire en la génération spontanée, la démonstration du théorème de Pythagore et la couleur de la robe de la belle-soeur au pique-nique de la semaine dernière ("Vert. -Non bleu. -Non vert. -Non bleu, je me souviens bien, c'est ma soeur quand même ! -Non vert, je le sais, je fais gaffe pour les relations familiales. -Non, bleu comme ça ! -Ah parce que tu appelles ça bleu ?"). Il ne faut pas oublier cela, à la base, la mémoire vive, c'est la source des disputes ménagères. Mais les informaticiens rêvent de recréer en silicium toute la magie des séances de bris de vaisselle, et ils se consolent en faisant des analogies. Ainsi, les ordinateurs disposent de mémoire vive. Ce sont des composants électroniques qui peuvent changer d'état sur commande, puis rester dans cet état tant qu'ils sont sous tension. Et quand il n'y a plus de tension, on ne sait plus ce qu'il se passe. Ça garde son état : c'est une mémoire, ça devient n'importe quoi quand c'est éteint : c'est vif. C'est de la mémoire vive.

Les électroniciens, qui sont des gens prosaïques et qui n'ont rien à voir avec EDF, ne parlent pas de mémoire vive : ils parlent de la mémoire en fonction de son type, de sa nature profonde. En général, ils causent de RAM (Random Access Memory), Mémoire à Accès Aléatoire. C'est un sigle confus, car bien évidemment, l'accès n'est pas aléatoire ! D'ailleurs, c'est bien dommage, ça nous aurait permis d'éviter un des gros problèmes de l'informatique (pour ceux que ça intéresse, penchez-vous donc sur les nombres pseudoaléatoires). Ce qui est aléatoire dans l'accès, c'est la prochaine case qui va être demandée, du point de vue de la mémoire. Contrairement à l'accès séquentiel où l'ordre de lecture est prédéterminé, le processeur peut demander n'importe quel contenu à tout instant.

III-2 - De la taille

Les chauffards de l'informatique existent. Ils fonctionnent à peu près de la même façon que les chauffards routiers, à ceci près qu'ils parlent de bus AGP là où d'autres parlent de carburateur double-corps. Et parfois, dans leur conversation, ils évoquent la quantité de mémoire dont ils disposent sur leur machine. Ce concours à celui qui a la plus grande nous apprend que les ordinateurs ne sont pas égaux en mémoire vive. Comme KarlOS permet d'exploiter un ordinateur, ce serait pas mal qu'il sache de combien il dispose pour faire joujou.

III-3 - Des périphériques

Nous écrivons les pixels à afficher dans la mémoire vive, dans une plage d'adresses donnée. Cette partie de la mémoire est particulière, et nous n'avons pas à y écrire les décimales de pi. C'est, en fait, un espace de dialogue avec la carte graphique. Conséquemment, nous en déduisons que certaines plages d'adresses sont réservées au bénéfice d'un périphérique. On dit, dans l'horrible jargon informatique, que le périphérique est mappé en mémoire. Mappé, cela vient de map, carte. C'est ce que nous voulons : une carte de la mémoire, pour savoir où il y en a et ce qu'on peut en faire.

III-4 - De la cartographie

Aux rétifs de la boussole, paix et amour. Un ordinateur ne connait ni haut, ni bas, ni nord, ni sud, pas plus que gauche ou droite. Il ne connait que devant et derrière. C'est linéaire. Au risque de continuer dans les platitudes, il n'y a qu'une dimension. L'IGN du PC est parfaitement au point et n'édite que des listes : de 0x0000 à 0xFFFF, je n'ai rien de répertorié. De 0x10000 à 0x2FFFF, la mémoire est réservée. Le reste n'existe pas.

L'IGN du PC est dans le BIOS. Moi, le jour où j'ai appris ça, j'ai dit : "Arg !". Oui, cela signifie qu'il faut récupérer cette carte de la mémoire avant de passer en mode protégé. Cela implique de le faire dans le chargeur de secteur d'amorçage. Vous savez, le secteur d'amorçage, celui qui ne peut faire que 510 octets de long ? Eh bien, on va se serrer un peu et on va y faire la carte de la mémoire.

III-5 - De la pratique

On se place dans le code du secteur d'amorçage, juste après le chargement de KarlOS à partir de la disquette. On se place là parce que cela nous évitera de changer la valeur de DX qui contient le lecteur sur lequel on travaille. De surcroît, cela garde le changement de mode vidéo le plus près possible du passage au noyau à proprement parler.

Il s'agit du service 0x15, fonction 0xE820. Pour les paramètres, accrochez-vous au pinceau ça déménage. EBX représente l'avancement de la construction de la carte mémoire. Il doit être à 0 au départ, et est censé retourner à 0 une fois toute la carte parcourue. EDX contient "SMAP", ce qui donne 0x534D4150. ECX contient la taille d'une entrée de la carte mémoire : 24 bits est bien, c'est la taille maximale possible actuellement, ce qui permet de ne rien rater et d'inclure de vieilles versions. ES contient le segment (version 20 bits d'adresse !) de notre carte, et DI son décalage.

Ceci étant rempli, on appelle l'interruption.

Étudions les valeurs de retour. EAX doit contenir "SMAP". Si la retenue a été posée, la liste a été parcourue dans son ensemble. Si EBX est RAZé, idem. ECX est censé contenir la taille de la zone mémoire citée dans la nouvelle entrée de la carte (donc si zéro, on zappe, les zones mémoire de taille nulle étant de très très peu d'intérêt). À partir de la version 3 de l'ACPI (Advanced Configuration and Power Interface) - interface avancée de configuration et de gestion de l'énergie - le bit de poids faible de la zone de 24 bits indique que la zone mémoire est à ignorer. À quoi sert d'indiquer une zone mémoire si c'est pour demander de l'ignorer, là est la question. Toujours est-il que nous l'ignorerons, en bons petits enfants sages. On testera aussi que M. BIOS ne nous a pas écrit que des zéros.

Une fois les retours traités, on recommence pour in fine avoir la carte totale.

Je vous donne les quelques octets qui font le boulot :

;;Infos mémoire
; INT 0x15, eax = 0xE820 : fonction BIOS 32 bits qui donne la carte de la mémoire
; ES:DI est l'adresse à laquelle elle écrit une entrée
mov     ax,     BOOT_SEG
mov     es,     ax
mov     di,     DonneesMemoire
xor     ebx,     ebx            ; EBX est un marqueur d'avancement pour INT 0x15, on commence donc par 0
xor     bp,     bp            ; BP stocke le nombre d'entrées de la table
.entree:
mov     edx,     0x534D4150    ; Place "SMAP" dans EDX
mov     eax,     0xE820
mov     [es:di + 20], dword 1; Par souci de compatibilité, on considère la future entrée comme une entrée ACPI 3.X valide
mov     ecx,     24            ; On demande 24 octets à INT 0x15
inc     bp                    ; On compte cette entrée
int     0x15
jc         .fin                ; Si la retenue est posée, c'est qu'on a atteint la fin
cmp     eax,     0x534D4150    ; Si EAX ne contient pas "SMAP", c'est une erreur
jne     .fin
test     ebx,     ebx            ; Si EBX est à 0, on a fini
je         .fin
jcxz     .entree                ; Si ECX est à 0, il s'agit d'une zone de taille nulle. Aucun intérêt.
cmp     cl,     20            ; Si on a moins de 24 bits
jbe     .sansExtension        ; on n'est pas en ACPI 3.X
test     byte [es:di + 20], 1; Sinon, est-ce que le bit "à ignorer" est placé ?
je         .entree
.sansExtension:
mov     ecx,     [es:di + 8]    ; get lower dword of memory region length
or         ecx,     [es:di + 12]    ; "or" it with upper dword to test for zero
jz         .entree        ; if length qword is 0, skip entry
add     di,     24
jmp     .entree
.fin:
mov     [NbEntreesCarteMemoire], bp    ; store the entry count

III-6 - De la bonne exécution de nos basses oeuvres

On est certes très forts et fiers de nous, mais mes bien chers frères, nous sommes humains et errare humanum est. Conséquemment, avant de nous plonger dans les joies indicibles de la gestion de la mémoire, allons-nous afficher cette carte. Et ça va se passer, tout le monde s'en doute, en mode protégé.

Pour que toutes ces données acquises de haute lutte soient plus manipulables par notre intellect, on va commencer par les recopier chez nous. Ceci nous permettra, plus tard, de considérer l'espace utilisé par le chargeur comme libre, ce qui nous évitera un bout de code perdu nulle part au milieu de la mémoire. Simple, on a déjà fait avec les informations vidéo.

Ensuite, ma foi, je propose de simplement afficher les 24 octets comme des nombres en hexadécimal. Cela a l'avantage de refléter la structure bit à bit de ces 24 octets. On a déjà les fonctions qui font cela, il suffit de les adapter à notre nouvelle façon d'afficher. On va en profiter pour déplacer des bidules dans notre alphabet, histoire de simplifier la procédure de conversion en chaîne. Comment ? C'est simple. Auparavant, par injonction divine de la table ASCII, en cas de débordement des sacro-saints chiffres arabes, on devait faire des additions pour retomber sur les caractères. Eh bien, mes amis, foin de ceci dorénavant : en mettant l'alphabet juste après les chiffres arabes, le tour est joué !

On exécute notre machine virtuelle, et on obtient des séries de nombres en hexadécimal. Super.

Ces nombres sont affichés en gros-boutiste, par quadruple-mot (de 32 bits). Il y a donc six quadruple-mots. Les deux premiers sont l'adresse de départ de la zone concernée. On est en 32 bits, bravo, le deuxième quadruple-mot doit donc être vide, ou 0. Notez qu'on est prêt pour le 64 bits de ce point de vue. Les deux suivants sont la taille de la zone en octets. On est toujours en 32 bits, donc le deuxième quadruple-mot est toujours à 0. Le quadruple-mot suivant n'est intéressant que pour son plus petit octet, dont je vous livre les valeurs possibles (librement traduit et reformulé depuis le wiki de OSDev).

- 1 : Mémoire utilisable, normale, RAS.

- 2 : Mémoire non utilisable, indisponible.

- 3 : Mémoire utilisée par l'ACPI, mais on peut la réclamer. ("All your memory are belong to us.")

- 4 : Mémoire utilisée par l'ACPI et tant pis.

- 5 : Vilaine mémoire.

Et le dernier quadruple-mot contient 1. Sinon, y'a pétard. S'il y a pétard, demandez de l'aide.

Hopopop ! Partez pas c'est pas fini. Dites, sur votre machine, réelle ou virtuelle, il y a combien de mémoire ? Pour les virtualistes, c'est dans la configuration de votre émulateur. Pour d'éventuels autres, ça doit se trouver quelque part : bon de livraison, coin de cervelle. Faites la somme des tailles données par notre carte. Vous devez avoir une somme qui tombe sur votre mémoire, enfin je veux dire, sur la vraie mémoire de votre machine. Si ce n'est pas la somme des types 1 et 3, ce doit être la somme de tous les types, ça dépend comment on compte. Alors, est-ce que "le compte est bon" ?

III-7 - Du code

L'ensemble du code de ce chapitre se trouve ici : Tuto12

IV - Remerciements

Commentez

Merci à l'équipe de developpez.com, et en particulier à ClaudeLELOUP pour sa relecture soigneuse.