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

Vous souvenez-vous du bon vieux temps ? Je vous parle du temps de l'autoexec.bat et des driver de quelques ko, le temps où on écrivait directement en mémoire vidéo, le temps de l'assembleur, du mode réel et des interruptions.

Hé bien cette époque bien que révolue nous a laissé des traces. Il est toujours possible de coder sans noyau, de faire son propre noyau, de se passer de système d'exploitation ou de booter directement sur une application en quelques secondes.

Boot loader

Choisissons un bootloader, grub par exemple, nous allons lui demander de lancer notre programme. Avec grub il existe plusieurs méthodes pour charger un os, entre autres :

  • Chainloader charge 512 octets, met le processeur en mode en mode réel, tel qu'il serait après le passage du bios. Pratique pour partir de zéro, mais 512 octet c'est un peu petit et nous n'avons pas vraiment envie de gérer le chargement de fichier sur le disque en plus.
  • Linux charge un noyau au format zimage ou bzimage et lui passe le contrôle en mode réel 16 bits. Il est possible de charger des fichiers de plusieurs méga avec le format bzimage.
  • Multiboot charge un noyau au format multiboot et lui passe le contrôle en mode protégé 32 bits après un certain nombre d'initialisations. On utilisera cette méthode une prochaine fois.

zImage

On commence par une solution simple à mettre en place, un exécutable au format zImage. Le format est décrit dans la documentation des sources du noyau linux : /usr/src/linux-2.X.XX/Documentation/i386/boot.txt. C'est dans ce format que nous allons développer un binaire bootable directement.

Le bootloader (grub) lit l'en-tête du fichier zImage puis pose la première section (attention, taille limitée à quelques dizaines de ko) à l'adresse 0x90000, Ensuite, il lit le reste du fichier et le place en 0x1000 (pour une zImage, limitée à 512ko) ou en 0x100000 (pour une bzImage). Ensuite, dans le cas de linux, le noyau s'amuse à redéplacer tout ça selon un processus plus ou moins compliqué décrit ici et ici. Attention, ne prenez pas ces documents pour argent comptant, ils sont un peu dépassés. En pratique nous n'irons pas si loin puisque nous resterons en mode réel et que 512ko nous seront largement suffisant (640Ko ça devrait suffire pour tout le monde !).

Nous allons donc faire simple et récupérer le code de boot de linux pour lancer notre petit projet. Le fichier qui contient le code de lancement ainsi que les en-têtes qui vont bien s'appelle header.S.

Attention, il est écrit en assembleur GNU (gas), celui-ci diffère beaucoup de l'assembleur intel. Le point le plus important est que les arguments sont inversés (source puis destination). header.S Apportons quelques modifications au fichier pour qu'il corresponde à nos besoins :

  • suppression des include
  • changement de la version de format pour se simplifier la vie
-               .word   0x0209          # header version number (>= 0x0105)
+               .word   0x0207          # header version number (>= 0x0105)
  • pas de payload (il faudra tout faire dans 4 secteurs (2Ko)
-payload_offset:                .long input_data
-payload_length:                .long input_data_end-input_data
+payload_offset:                .long 0
+payload_length:                .long 0
  • suppression des appels à puts pour éviter les dépendances inutiles
-       calll   puts
  • ajout des #define correspondant à nos besoins (surtout pour que ça compile)
+#define DEF_SYSSEG       0x1000                                  
+#define DEF_SYSSIZE  0x200
+#define ASK_VGA          0xfffd          /* ask for it at bootup */
+#define STACK_SIZE      512     /* Minimum number of bytes for stack */
+#define CONFIG_PHYSICAL_ALIGN 0x200000
+#define COMMAND_LINE_SIZE 2048

Ce fichier se termine par un appel à la fonction cmain qu'on codera en C, bien plus sympa à écrire que l'assembleur.

Fractint

Comme nous voulons avoir des résultats rapidement, nous allons choisir un projet simple, dessiner l'ensemble de Mandelbrot en vga. Résumons l'algorithme en 3 étapes : initialiser le mode graphique, afficher la fractale et attendre.

On utilise gcc (du projet gnu pour ceux qui suivent ;-), mais son compilateur C ne génère que de l'assembleur 32 bits. Donc la ruse de sioux est de préfixer le code par la directive suivante pour que l'assembleur le transforme code compatible 16 bits (nécessaire puisque nous sommes en mode réel) :

asm(".code16gcc");

Pour le mode graphique, choisissons le mode vga 0x13 (320x200x8), il est facile à utiliser car chaque pixel est mappé sur une adresse en RAM et donc adressable directement en mode réel à l'adresse 0xA0000 (A000:0000 pour les habitués de l'assembleur Intel 16 bits). Les vieux savent bien que la carte graphique se gère avec l'interruption 10h, fonction 0 pour choisir le mode vidéo.

C'est l'occasion de regarder comment on incruste de l'assembleur dans du code C avec gcc :

/* Je vous fais la version longue */
/* Le code assembleur paramétré (d'où les %%) */        asm ("pushl %%ebp; pushw %%ds; int $0x10; popw %%ds; popl %%ebp"
/* L'affectation des valeurs de retour  */                      :
/* L'affectation des paramètres */                              : "a" (mode)
/* La liste des registres à sauvegarder */                      : "ebx", "ecx", "edx", "esi", "edi");

On trouve en ligne les détails pour faire du code assembleur en gcc. Remarquez que si vous n'avez aucun '_:_' (pas de paramètre) les %% doivent être remplacés par des %.

Maintenant, la fonction pour modifier la palette de couleurs (8 bits, 256 couleurs, une palette) avec des valeurs RGB sur 6 bits :

/* la technique d'origine pour modifier la palette vga */
/* on modifie directement des registres de la carte graphique */
        asm( "outb %%al,%%dx" : : "a" (idx), "d" (0x03c8) );
        asm( "outb %%al,%%dx" : : "a" (r), "d" (0x03c9) );
        asm( "outb %%al,%%dx" : : "a" (g), "d" (0x03c9) );
        asm( "outb %%al,%%dx" : : "a" (b), "d" (0x03c9) );

Et la fonction pour modifier un pixel :

/* calcul de la position du pixel en mémoire vidéo */
        int pos = 320*y+x;
/* Technique à la mano car gas ne supporte pas l'adressage 16bits avec des segments
 * - on passe par ds qu'on doit sauvegarder
 * - la mémoire vidéo du mode vga 13h commence en A000:0000
 */
        asm ("push %%ds; movw %%ax,%%ds; movb %%cl,(%%bx); pop %%ds"
                : : "a" (0xA000), "b"(pos), "c" (color));

Le reste est un algorithme basique de calcul de Mandelbrot. Pour rappeler quelques souvenirs :

  • c = (x,y)
  • p0 = (0,0)
  • p(n+1) = p(n)^2 + c
  • plus p diverge vite, plus on est loin de l'ensemble (-> choix de la couleur)

Compilation

Il faut maintenant compiler tout ça pour en faire un binaire au format zImage.

On compile avec gcc en faisant bien attention à ne pas inclure la libc (et à spécifier une architecture 32 bits pour les amd64) :

CFLAGS="-fno-builtin -nostdinc -O2 -I. -Wall -m32"
gcc $CFLAGS -c header.S
gcc $CFLAGS -c kernel.c

Ensuite, il faut faire l'édition de lien (on fait toujours attention à la libc), nous avons besoin d'un script (setup.ld) pour maîtriser complètement le binaire sortant. Il est repiqué de linux, avec une instruction supplémentaire à la fin pour aligner la taille du binaire à celle précisée dans l'en-tête :

LDFLAGS="-nostdlib -Wl,-N"
gcc $LDFLAGS -o mandel.exec -T setup.ld header.o kernel.o

Et on extrait le binaire final de notre binaire au format elf :

# extraction du binaire
objcopy -O binary mandel mandel.exec

Grub, qemu

Il nous faut maintenant tester notre œuvre. Plutôt que de devoir rebooter notre vraie machine, nous allons utiliser une machine virtuelle complète (qemu, vmware, virtualbox ...), dans laquelle on installe grub.

Et dans votre menu (ou dans le shell grub) :

kernel (hd0,0)/boot/mandel

C'est jôliii ! mandel1.png

Pour ceux qui veulent tester, il y a en pièces jointes, les sources ainsi qu'une image disque toute prête pour qemu contenant les sources, le binaire compilé, ainsi qu'un grub préinstallé avec un menu (et le contenu du prochain article avec, mais chut). Pour ceux qui n'ont pas tout suivi, essayez de lire les sources, ca peut être instructif.

$ qemu mandel.dd

Dans le prochain numéro, nous ferons la même chose en plus grand, plus beau et en mode protégé.

PJ :