Skip to content

Linux Attitude

Le libre est un état d'esprit

Archive

Catégorie: Développement

Niveau : Star Star Star Empty Empty
Résumé : perlconsole

Vous développez régulièrement vos scripts en perl ? Si oui : lisez ceci ou cela ou encore d'autres choses plus ou moins utiles.

Si oui vous devez faire des tests de temps en temps. Je me suis habitué à faire des perl -e ou perl -pe pour des petites choses. Mais il existe un outil bien plus pratique : perlconsole.

Son usage est simple :

$ perlconsole

Pour le debug, cet outil est sympa puisqu'il permet de taper les commandes une par une et d'en avoir la valeur de retour.

Pour le développement cet outil est parfois un peu lourd puisqu'il renvoie la valeur de retour de toutes les commandes tapées, même si cela ne nous intéresse pas vraiment.

Autre fonctionnalité qui ravira certains, les valeurs de retours peuvent être affichées sous différentes formes. Ce qui veut dire qu'il est possible d'avoir un dump complet du résultat de chaque commande. Il suffit pour cela d'entrer la commande suivante pour les habitués de Data::Dumper :

:set output=dumper

Mais il existe aussi une sortie yaml, que je trouve personnellement plus lisible pour des structures ne dépassant pas une page.

:set output=yaml

Niveau : Star Star Empty Empty Empty
Résumé : perl

Perl fait beaucoup de choses, trop, tellement que certaines sont complètement inutiles

Il est possible de changer la numérotation des tableaux :

# le premier élément d'un tableau sera maintenant le numéro 42
$[=42;

Il est possible d'allouer de la mémoire inutilisée :

# alloue 64Mio de mémoire pour que perl puisse les utiliser quand il meurt a cause du manque de mémoire !!
# ce qui ne l'empêche pas de mourir bien sur
$^M=64<<20; 

Les sections BEGIN {} et END {} sont des blocs très utiles qui s'écrivent tels quel, mais il peuvent aussi être écrits comme des méthodes :

sub BEGIN {}

Perl disposes de modules en provenance du CPAN. Parmi ces modules on trouve les modules Acme, dont voici quelques exemples pour bien en comprendre l'utilité :

  • Acme::Anything qui permet de charger des modules qui n'existent pas
  • Acme::Error qui permet d'afficher toutes les erreurs en capitales.
  • Acme::Godot qui vous permet d'attendre Godot
  • Acme::EyeDrops pour convertir votre code perl en ascii art (fonctionnel en perl), très joli d'ailleurs
  • Acme::Code::Police supprime toute erreur de code qui n'utiliserait pas strict en supprimant le code.

Niveau : Star Star Star Empty Empty
Résumé : perl

Quelques trucs et astuces de perlistes en vrac. Pour des vrais astuces de pro régulières, n'hésitez pas à visiter le site des moines de perl.

Initialiser un tableau de chaînes sans avec une syntaxe légère, très lisible quand on n'a que des mots, le séparateur étant les caractères blancs, incluant le retour à la ligne :

@tableau = qw( mot1 mot2 mot3 );

Passer du mode script de quelques lignes au mode développement long. Cela vous oblige à déclarer et initialiser toutes vos variables entre autre :

use strict;

Le debug facile :

use Data::Dumper;
# variable peut être n'importe quoi, toutes les sous-sous parties sont écrites :
print Dumper($variable,\@variable);
print Dumper(\%variable);

L'aide en ligne facile, ça fait tout de suite classe dans un script (format) :

use Pod::Usage;
# imprime le contenu de SYNOPSIS et termine le programme avec un code d'erreur 1
pod2usage(1);
# attention, pod est chatouilleux sur les retours à la ligne avant et après les =

=head1 SYNOPSIS

commande  [-o|--option=<valeur>]

=cut

La page de manuel facile (pas seulement le SYNOPSIS comme précédemment) :

# où fichier.pl est le code précédent
$ pod2man fichier.pl > fichier.1
$ man ./fichier.1

Et le dernier pour la route, le parsing d'options facile, très classe, surtout en combinaison avec pod2usage (format) :

use Getopt::Long;
my $options = {
        option => 'defaut'
};
GetOptions( $options, qw(
        option|o=s
));

Niveau : Star Star Star Empty Empty
Résumé : chmod +x test.c

Après une technique utilisant du perl, voici une nouvelle façon d'utiliser du C aussi simplement qu'un script.

Cette fois nous évitons l'usage de perl, on se passe donc de ses avantages en terme de parsing. Par contre, on utilise un en-tête court et le code fonctionne avec n'importe quel code valide en C. Comme d'habitude, on fait propre et on renvoie le bon code de retour.

#!/bin/sh
tail -n +4 $0 | gcc -Wall -o /tmp/cscript.$$ -x c - && /tmp/cscript.$$ $*
ret=$? ; rm -f /tmp/cscript.$$ ; exit $ret
//
// Code C
//
#include <stdio.h>
int main(int argc, char** argv)
{
        printf( "Appel de %s avec %d arguments\n", argv[0], argc-1 );
        return 0;
}

Et maintenant on teste pour prouver que ça marche :

chmod +x test.c
./test.c 1 2 3 && echo "OK" | | echo "KO"

Niveau : Star Star Star Star Empty
Résumé : SSE

Aujourd'hui l'avenir (et un peu le passé aussi). Vous souvenez-vous des cray ? Ces machines surpuissantes qui avaient au moins la puissance de calcul d'une TI92. Comment faisaient-ils pour avoir une telle puissance ? Il faisaient des calculs identiques en parallèle et multipliaient ainsi par n le nombre d'opérations. C'est ce qu'on appelle une architecture vectorielle.

Intel dans sa grande bonté a commencé à mettre en place une telle architecture dans ses processeurs. Tout d'abord avec les instructions MMX. Ces instructions utilisent les registres servant au calcul en virgule flottante (quelques transistors de gagnés), lesquels font 64 bits. On peut ainsi exécuter des instructions identiques simultanément sur 4 fois 1 octets ou sur 2 fois 2 octets. Ce qui est bien, mais pas top.

Le SSE est une amélioration du concept. Tout d'abord on utilise de nouveaux registres (ce qui libère les calculs en virgule flottante) et on leur donne une taille de 128bits. Du coup on peut manipuler deux fois plus d'entiers et cela devient intéressant pour les flottants. On peut caser 4 flottants simple précision et 2 flottants double précision dans 128 bits. Le SSE s'arrête aux 4 flottants et le SSE2 ajoute quasiment toutes les autres combinaisons possibles (flottants et entiers).

Version normale

Comme nous sommes joueurs, nous allons jouer à améliorer notre code de calcul de Mandelbrot. Mais cette fois, nous allons le laisser tourner sous linux. Comme ne sommes plus dans notre monde, mais sur un vrai système, nous allons devoir faire quelques modifications. Nous allons continuer à écrire directement en mémoire, donc on garde nos fonctions d'écriture de pixels, mais cette fois dans une mémoire allouée avec malloc. Ajoutons le support de l'écriture d'un des format d'image les plus simples, le BMP, pour stocker notre image dans un fichier et pouvoir admirer le résultat. Pour cela on prend la première documentation venue.

/* Ecriture de l'image dans un fichier bitmap (on écrit l'en-tête kivabien puis les données) */
void writebitmap(char* filename, char*bitmap, int width, int height);

Et voilà, cela nous donne une première version du code quasiment identique à la précédente et donc non optimisée pour le SSE (disponible à la fin de l'article).

Version SSE

Commençons à travailler en découvrant les instructions du sse. Celles-ci sont rendues disponibles en C à travers les en-têtes qui vont bien. Ouf, cela nous évitera de coder directement en assembleur :

/* mm=mmx   xmm=sse  emm=sse2   pmm=sse3 (rien que de très logique ...) */
#include <emmintrin.h>

Il est possible de lire ces en-têtes pour savoir quelles sont les instructions et les types disponibles. C'est un peu long et assez peu instructif lorsqu'on ne connait pas la base du sse, mais ils peuvent servir de référence quand on sait ce qu'on cherche. Ils se trouvent dans /usr/lib/gcc/x86_64-linux-gnu/4.1.2/include/ (à adapter à votre cas). Les types et fonctions du sse sont codifiés.

Les opérations sont construites comme suit _mm_XXXX_YY :

  • _mm est un préfixe
  • XXXX est le nom de l'instruction en assembleur (à quelques variations près)
  • YY est le type des opérandes manipulés :
    • sd : double seul (1 double qui n'occupe pas tout le registre)
    • pd : double packé (2 doubles dans un registre)
    • ss : flottant seul (1 flottant qui n'occupe pas tout le registre)
    • ps : flottant packé (4 flottants dans un registre)
    • ...

Les types de données sont aussi définis selon un certain schéma :

  • __m128 : registre de 128 bits
  • __m128i : registre de 128 bits destiné au calcul entier
  • __m128d : registre de 128 bits destiné au calcul flottant
  • __v4sf : vecteur de 4 simple flotants (4 * 32 bits)
  • __v2di : vecteur de 2 entiers doubles (2 * 64 bits)
  • ...

Notez que d'un point de vue binaire, ils sont tous identiques et font 128bits.

Maintenant nous allons modifier le code pour qu'il calcule les points 4 par 4. On va donc changer nos 2 fonctions de calcul en :

/* conversion de coordonnées 4 points par 4 */
void convert_i2d4(int x, int y, __v4sf *ppx, __v4sf *ppy)
/* une itération du calcul pour 4 points */
__v4sf iteration4(__v4sf cx, __v4sf cy, __v4sf *ppx, __v4sf *ppy)

On utilise un des types précédents : __v4sf. C'est un vecteur de 128 bits contenant 4 flottants simple précision. Mais comme nous n'utilisons plus des types standards du C, nous devrons utiliser des fonctions à la place des opérateurs +, -, / et *, ce qui rend le code un peu plus lourd à lire et à écrire, par exemple :

        /* t = p^2 + C */
        tx = _mm_add_ps( _mm_sub_ps(_mm_mul_ps( *ppx, *ppx),  _mm_mul_ps(*ppy, *ppy)) , cx);
        ty = _mm_add_ps( _mm_mul_ps(two, _mm_mul_ps(*ppx, *ppy)), cy);

On y ajoute quelques constantes globales du fait que les constantes n'existent pas pour les type vectoriels :

__v4sf two = _mm_set_ps(2,2,2,2);

Et enfin, on doit adapter un peu notre algorithme pour qu'il ne s'arrête que lorsque les 4 points ont tous dépassés le seuil. Pour que la bonne couleur soit bien enregistrée séparément pour chaque point, on va faire la comparaison sur les 4 points grâce à _mm_cmple_ps qui renvoit 0 ou -1 sous forme de vecteur d'entier. Il nous suffit d'additionner les résultats pour récupérer la couleur de chaque point telle que nous l'avions définie plus tôt.

    /* calcul */
    radius = iteration4(cx, cy, &px, &py);
    /* on compare au rayon max, la valeur de retour est constituées de 4 entiers valant 0 ou -1 */
    jj = (__m128i)_mm_cmple_ps(radius, eight);
    /* et on additionne tous les -1 */
    ii = _mm_add_epi32(ii, jj);
    /* un int fait 32 bits sur les i386 et les x86-64 ... */
    ptr = (int*)&jj;
    /* il y a un moment où ce n'est plus la peine de calculer */
    if(ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 0 && ptr[3] == 0)
        break;
 

Le code complet est disponible à la fin de l'article.

Résultat

Mais cela vaut-il le coup ? Oui, et nous allons le montrer. Tout d'abord sur un processeur 32 bits. Nous allons comparer plusieurs combinaisons. Avec sse, sans sse, avec différents niveaux d'optimisation, avec notre code spécifique, et sans.

exécution sur un i386 (en secondes)
Optimization - -O1 -O2 -O3
Gcc parameter - -msse2 - -msse2 - -msse2 - -msse2
normal code 6.20 6.40 3.4 3.5 2.95 2.65 2.60 2.35
SSE code - 4.75 - 0.91 - 0.55 - 0.50

Les résultats sont assez parlants. Que peut-on en déduire ? Que le sse ne semble pas très performant en l'absence d'optimisation (nosse.c et colonnes sans optimisation ou avec -O1).

Ensuite, que même sans utiliser le sse, gcc est lui-même capable de nous faire gagner beaucoup en performances avec ses optimisations. Mais gardez à l'esprit que cela est surtout valable parce qu'on utilise beaucoup de calculs en virgule flottantes et que nos fonctions prennent de gros arguments.

Enfin, que la modification de notre algorithme pour profiter du sse nous a permis de faire mieux que x4 sur du code déjà optimisé par gcc. En effet, gcc optimise ce qu'il peut, mais il lui est très difficile de changer un algorithme. Il est prévu que de nouvelles versions de gcc sachent vectoriser automatiquement des calculs, mais il faudra tout de même faire attention à la façon dont on code.

exécution sur un x86-64 (en secondes)
Optimization - -O1 -O2 -O3
Gcc parameter - -msse2 - -msse2 - -msse2 - -msse2
normal code 2.10 2.10 1.05 1.05 0.76 0.76 0.72 0.72
SSE code 2.10 2.10 0.30 0.30 0.30 0.30 0.30 0.30

Et enfin regardons le même code sur un processeur 64 bits. Tout d'abord on remarque que ma machine 64 bits est plus puissante que la précédente :-) Ensuite, on constate qu'il n'y a pas de différence entre l'option sse et sont absence. Et heureusement puisque tous les processeurs 64 bits ont le sse par défaut et le compilateur ne se gène pas pour l'ajouter lui-même. On remarque que comme pour le code 32 bits, il y a un facteur 3 entre l'option -O3 et l'absence d'optimisation pour le code normal.

Par contre, les différents niveaux d'optimisation au delà de -O1 n'influent plus sur la vitesse d'exécution. Je n'ai pas d'explications à cela.

Et enfin on constate que le sse a réduit la différence de puissance entre les 2 machines.

Pour ceux qui se demanderaient si on mesure les bonnes choses, un test écrivant tous les pixels en noir a été fait, et il ne prend pas plus de 0.03s sur ces plate-formes (toujours moins de 10%). Ce qui veut dire que nos comparaisons sont pertinentes (et qu'il n'est pas nécessaire d'optimiser cette partie).

Vous voulez tout savoir sur le SSE ? Arstechnica a publié un très bon article sur le sujet. Sinon, vous avez toujours la liste détaillée des instructions.

PS : Intel trouvant que la vectorisation est une bonne chose et voyant que tout cela marche bien a prévu d'ajouter les instructions AVX dans ses prochains processeurs. En gros, c'est la même chose que le SSE mais sur 256 bits cette fois.

PJ :

Niveau : Star Star Star Star Empty
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 :

Niveau : Star Star Star Star Empty
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 :