Niveau :      
Résumé : grub ; noyau ; multiboot ; mandelbrot

Suite à de précédentes bidouilles, on veut faire mieux.

Multiboot

Donc grub peut booter plusieurs formats, l'un d'entre eux est le multiboot, un format de fichier bootable défini par les développeurs de grub espérant qu'il soit adopté par différentes distributions. Il souffre d'un certain nombre de faiblesses et n'est pas encore vraiment un standard. D'autant que grub lui-même ne supporte pas encore complètement la spécification pourtant très courte. Si vous ne voulez pas lire toute la spécification, allez directement au chapitre 4 où on trouve un code d'exemple prêt à compiler pour vous lancer dans le développement.

Vous trouverez un autre exemple chez quelqu'un qui s'est amusé à coder un space invaders bootable directement à partir de la spécification multiboot.

Alors c'est parti, reprenons notre projet précédent. Faites attention, le processeur est dans un état particulier lorsqu'on vous laisse la main (lisez le chapitre 3.2), à vous de vous en accommoder.

Mode graphique

Tout d'abord on voudrait changer le mode graphique, et si possible pour mieux que la première fois. Pour cela, on va utiliser le standard vesa 2.0 qui standardise des modes avec une plus grande définition et qui ajoute de nouvelles fonctions au bios pour manipuler la carte.

Choisissons le mode 0x118 (1024x768x24) avec 16 millions de couleurs. Plus besoin de gérer un palette puisqu'on écrit directement les couleurs RGB à utiliser pour le pixel. On aura tout de même une fonction de palette puisqu'on n'utilise qu'un petit nombre de couleurs placées sur une échelle.

Le problème c'est que vesa n'a pas prévu qu'on puisse appeler les fonctions de la carte en mode protégé sans passer au moins une fois par une interruption en mode réel. Multiboot est bien gentil, il a prévu le cas, malheureusement, grub n'implémente pas les fonctions qu'il devrait pour le faire pour nous. Un patch est disponible pour ceux qui voudrait quand même cette fonctionnalité.

Il nous faudra donc faire nous-même ces interruptions. Pour survivre au mode réel nous devrons être sous le premier Mo de RAM, or grub nous place juste au dessus (en 0x100000). Notre premier bout de code aura donc pour but de nous déplacer en dessous. Pour cela on commence par prendre l'en-tête multiboot (beaucoup plus simple que pour les zImage) et on écrit le code pour se déplacer en 0x1000 (choix arbitraire) :

        /* on se copie ailleurs */
        mov     $KERNEL_SIZE,%ecx /* en octets */
        mov     $KERNEL_ORIG,%esi /* source juste après ce secteur */
        mov     $KERNEL_DEST,%edi /* destination sous les 1Mo */
        rep movsw

        /* et on va dans notre nouveau noyau */
        ljmp    $0x8,$KERNEL_DEST

Bien, maintenant, il faut passer du mode protégé au mode réel et inversement pour pouvoir faire des interruptions. Choisissons comme toujours de ne pas réinventer la roue et repiquons le code de grub qui dispose déjà de ces fonctions en assembleur. Elles s'appellent real_to_prot et prot_to_real. Pour ceux qui voudraient comprendre comment faire, c'est partout sur le net. Comme le mode protégé a besoin de descripteurs de segment, on n'oublie pas de les définir aussi (lire la fin du fichier asm.S) .

Du coup on peut utiliser 2 fonctions vesa bien utiles dans notre code, set_mode pour mettre en place notre mode graphique, et get_modeinfo pour savoir comment l'utiliser. En effet, on voudrait, comme avant pouvoir accéder directement à la mémoire vidéo pour y mettre nos pixels. La fonction get_modeinfo est là pour ça, elle utilise la fonction 0x4F01 du bios. On lui passe une adresse dans laquelle elle va écrire une structure de données nous informant sur le mode choisi. On choisit l'adresse de cette structure au pif puisqu'on n'a pas de gestion de la mémoire et qu'on a toute la ram pour nous. On prend 8000:0000.

La structure renvoyée contient l'adresse du mapping de la mémoire vidéo en 28h, ainsi que ses dimensions. Cela nos permettrait de changer le mode dans notre code facilement (notez les conversions de type pour lire le bon nombre d'octets) :

        /* on récupère les infos sur le mode qui nous intéresse */
        get_modeinfo(0x118);
        /* conversion de l'adresse du format [segment:offset] au format à plat */
        data = (MODEINFO_SEG << 4)+MODEINFO_OFF;
        /* on joue sur les converstions pour la lecture du bon nombre d'octets en ram */
        video = (unsigned char*) *(unsigned int*)(data + 0x28);
        width = (int) *(unsigned short*)(data + 0x12);
        height = (int) *(unsigned short*)(data + 0x14);

Code

On repart du code de l'article précédent. On y apporte quelques modifications techniques liées au changement de mode. Par exemple l'écriture d'un pixel se résume à l'écriture des 3 couleurs au bon endroit en ram :

        int pos = bpp*(x + y*width);
        /* et on ecrit directement en mémoire vidéo */
        video[pos] = r;
        video[pos+1] = g;
        video[pos+2] = b;

Pour ceux qui veulent aller plus loin dans la compréhension du mode protégé, ça aurait pu aussi être l'occasion de mettre en place un segment séparé pour écrire en mémoire vidéo avec la commande lgdt.

On peut imaginer y ajouter des tâches, par exemple une sorte de thread pour gérer le clavier. Mais la ça commence à devenir complexe et il vaudrait mieux lire des OS existants pour bien comprendre tout ça. Par exemple il existe minix qui a été fait dans un but pédagogique.

Compilation

Une autre chose change, c'est la compilation. Puisque le format multiboot est plus simple, nous n'avons plus besoin du script setup.ld. Par contre, puisque notre binaire contient deux morceaux (souvenez vous de la copie sous les 1Mo en début d'article), on devra modifier notre compilation.

Ce qui se résume en 2 partie, une pour le code de copie fait pour tourner en 0x100000 :

LDFLAGS1="-nostdlib -Wl,-N -Wl,-Ttext -Wl,100000 -m32"
gcc $LDFLAGS1 -o boot.exec boot.o
objcopy -O binary boot.exec boot

Et une partie contenant tout le reste du code, qu'on va placer en 0x1000 :

LDFLAGS2="-nostdlib -Wl,-N -Wl,-Ttext -Wl,1000 -m32"
gcc $LDFLAGS2 -o kernel.exec start.o asm.o kernel.o debug.o
objcopy -O binary kernel.exec kernel

Et enfin une dernière copie qui va concaténer les 2 binaires :

# on force la taille pour éviter les problèmes avec le bootloader
dd if=/dev/zero of=mandel2 bs=256 count=32 2>/dev/null 
# la premiere partie
dd if=boot bs=256 count=1 of=mandel2 conv=notrunc 2>/dev/null
# la 2e partie a une position fixée dans le code de la première (boot.S + header.h)
dd if=kernel bs=256 seek=1 of=mandel2 conv=notrunc 2>/dev/null

Run

Encore une fois, il suffit de préciser le fichier à grub pour qu'il le boote :

kernel         (hd0,0)/boot/mandel

Vous trouverez les sources en pièce jointe de cet article, et en complément une image disque contenant les 2 projets avec un grub installé qui fonctionne directement sur qemu.

$ qemu image.dd

Bravo à vous !

mandel2.png

PJ :