Catégorie : Utilitaires     Tags :      

    A chaque fois que vous démarrez un système d’exploitation, vous l’utilisez ; peut-être même sans le savoir. Son nom est peut-être GRUB [1], LILO [2], ELILO [3], yaboot [4] ou encore BootX [5]. Nous allons voir ici quels en sont les principes en prenant pour exemple un des plus simples : EMILE [6]. Vous ne l’utiliserez sûrement jamais : l’architecture sur laquelle il s’utilise n’est plus commercialisée depuis 1995. En effet, EMILE est un chargeur d’amorçage pour linux/m68k [7] sur Macintosh à base de processeurs de la série 680x0.

    Principes fondamentaux

    La première des qualités d’un système d’amorçage (ou bootloader) est d’avoir la capacité de prendre le contrôle de l’ordinateur lorsqu’on l’allume. Ce résultat est généralement obtenu en plaçant le système d’amorçage dans le secteur d’amorçage du disque de démarrage. On verra que cela n’est pas aussi simple qu’on peut le penser. Ensuite, le rôle du système d’amorçage est de charger en mémoire l’image d’un noyau et quelquefois un RAMDISK (ou comme GRUB les nomme : des modules). Les plus complexes permettent à l’utilisateur de choisir parmi une liste préétablie. Une fois cette tâche effectuée, il doit placer le système dans un état permettant de démarrer le noyau tout en lui passant quelques paramètres, tels que la ligne de commande ou l’adresse du RAMDISK. À ce point, il ne lui reste plus qu’à passer la main : placer le pointeur d’instruction du processeur sur le point d’entrée du noyau chargé.

    Prise de contrôle

    La prise de contrôle se fait par l’intermédiaire du secteur d’amorçage. Le nombre et la taille des secteurs d’amorçage dépendent du média (disquette, disque dur, CD) et de l’architecture (PC, Mac ou autres). Nous allons étudier ici le cas du secteur d’amorçage d’une disquette pour Macintosh. La taille d’un secteur physique est 512 octets, et le secteur d’amorçage est composé de deux secteurs physiques. Nous avons donc 1024 octets pour stocker notre système d’amorçage... mais peut-on stocker un système d’amorçage digne de ce nom dans 1024 octets ? La réponse est malheureusement "non". Alors, comment faire ?

    Premier niveau

    La solution consiste à diviser le chargeur d’amorçage en deux parties : un "niveau 1" et un "niveau 2". Le rôle du niveau 1 est de charger le niveau 2 tout en étant assez petit pour être contenu dans le secteur d’amorçage. Pour illustrer cela, nous allons prendre pour exemple le niveau 1 d’EMILE.

      101 start:
      102 	/* Allocate Memory for second stage loader */
      103
      104 	lea	ioReqCount(%pc),%a0
      105 	move.l	(%a0), %d0
      106 	add.l	#4, %d0
      107 	NewPtr
      108 	move.l	%a0, %d0
      109 	add.l	#3, %d0
      110 	and.l	#0xFFFFFFFC.l, %d0
      111
      112 	/* save result in the ParamBlockRec.ioBuffer */
      113
      114 	lea	ioBuffer(%pc),%a0
      115 	move.l	%d0,(%a0)
      116
      117 	/* Now, we load the second stage loader */
      118
      119 	lea	param_block(%pc),%a0
      120 	PBReadSync
      121
      122 	/* call second stage bootloader */
      123
      124 	move.l	ioBuffer(%pc),%a0
      125 	jmp	(%a0)

    Comme on le voit, le principe est simple : on alloue la mémoire (NewPtr) et on charge en mémoire (PBReadSync) le niveau 2. Ces opérations sont effectuées en faisant appel à deux traps contenus dans la ROM du Macintosh :

       19 .macro NewPtr
       20 	.short 0xA11E
       21 .endm
       22
       23 .macro PBReadSync
       24 	.short 0xA002
       25 .endm

    Les informations à connaître pour être capable de charger le niveau 2 sont sa taille et sa position sur la disquette. Ces informations sont, elles aussi, stockées dans le secteur de démarrage, directement dans la structure utilisée par PBReadSync pour lire les secteurs de données (param_block). IoReqCount est la taille, ioBuffer contient l’adresse en mémoire où stocker le niveau 2. Une fois les données chargées, le niveau 1 passe la main au niveau suivant en sautant au point d’entrée du niveau 2.

    Second niveau

    Le niveau 2 est donc le système d’amorçage en lui-même. C’est avec lui que vous allez interagir, pour modifier la ligne de commande par exemple. C’est lui qui va préparer la machine et charger les fichiers nécessaires en mémoire. Il s’exécute en mode "superviseur" et a donc tous les droits.

    Lecture des fichiers

    Maintenant que le système d’amorçage a pris le contrôle de la machine, il doit mettre en place en mémoire les différentes structures. Pour cela, il faut charger à partir du disque l’image du noyau. La lecture de cette image à partir de la disquette se faisant de la même manière que l’a fait le niveau 1 pour lire le niveau 2, nous allons plutôt nous attarder sur la lecture d’un fichier à partir d’un disque dur.
    La problématique ici est de retrouver les données d’un fichier. En effet, celles-ci ont été stockées à divers endroits du disque par le système de fichier en fonction des blocs de données disponibles dans la partition. Il existe deux solutions à ce problème : la première est d’ajouter un pilote permettant de lire le fichier en décodant la structure du système de fichiers, la seconde consiste à encoder la liste des blocs à lire dans le système d’amorçage. L’inconvénient de la première solution est qu’il existe différents types de système de fichiers et donc différents algorithmes pour accéder aux données du fichier. Des systèmes d’amorçage comme GRUB ont pris le parti d’intégrer cette méthode. EMILE utilise la seconde méthode, plus simple et déjà éprouvée par LILO, mais qui a l’inconvénient de nécessiter la mise à jour du niveau 2 à chaque fois que l’image du noyau est modifiée (lors d’une mise à jour du système, par exemple). Cette méthode nécessite la présence d’un outil tournant sous le système cible. Cet outil a pour tâche d’établir la liste des blocs utilisés par le fichier et de l’encoder dans le niveau 2. La difficulté ici est d’établir cette liste sans avoir à décoder directement le système de fichier ; si c’était le cas, nous nous retrouverions dans le premier cas de figure. Heureusement, Linux fournit un ioctl() permettant de récupérer le numéro de bloc physique dans la partition à partir du numéro de bloc logique dans le fichier. Le principe est le suivant :

     142   /* get first physical block */
     143
     144   last_physical = 0;
     145   ret = ioctl(fd, FIBMAP, &last_physical);
     146   if (ret != 0) {
     147           perror(“ioctl(FIBMAP)”);
     148           return -1;
     149   }
     150
     151   zone = last_physical;
     152   aggregate = 1;
     153
     154   /* seek all physical blocks */
     155
     156   current = 0;
     157   for (logical = 1;
     158        logical < (st.st_size + block_size - 1) / block_size;
     159        logical++) {
     160           physical = logical;
     161           ret = ioctl(fd, FIBMAP, &physical);
     162           if (ret != 0)
     163                   break;
     164           if (physical == last_physical + 1) {
     165                   aggregate++;
     166           } else {
     167                   ADD_BLOCK(first_block + zone * sectors_per_block,
     168                             aggregate * sectors_per_block);
     169                   zone = physical;
     170                   aggregate = 1;
     171           }
     172           last_physical = physical;
     173   }
     174
     175   ADD_BLOCK(first_block + zone * sectors_per_block,
     176             aggregate * sectors_per_block);

    On appelle donc ioctl() avec la requête FIBMAP qui prend en entrée le descripteur du fichier à analyser et un pointeur sur une variable contenant le numéro de bloc logique, en sortie ce pointeur pointera sur le numéro de bloc physique dans la partition. La particularité de l’algorithme précédent est qu’il essaye d’encoder des séquences de blocs consécutifs. De plus, comme le système d’amorçage n’a pas la connaissance des partitions, l’outil doit ajouter au numéro de bloc obtenu un incrément correspondant au début de la partition (first_block) pour avoir un numéro de bloc relatif au début du disque. Le début de partition est fourni par une autre requête ioctl() avec le paramètre HDIO_GETGEO :

      70    ret = ioctl(fd, HDIO_GETGEO, &geom);
      71    if (ret == -1)
      72    {
      73            fprintf(stderr, «%s: ioctl(HDIO_GETGEO) fails: %s»,
      74                            dev_name, strerror(errno));
      75            return -1;
      76    }
     ...
      88    *first_block = geom.start;

    La liste des blocs est ensuite écrite à un endroit défini dans le niveau 2 (et est donc chargée en même temps que le niveau 2 par le niveau 1). Lorsque le niveau 2 va vouloir trouver le bloc physique du disque associé à un numéro de bloc logique du noyau (ou du RAMDISK). Il va utiliser cette liste avec l’algorithme suivant :

      15 static unsigned long seek_block(container_FILE *file)
      16 {
      17     struct emile_container *container = file->container;
      18     ssize_t current;
      19     int i;
      20     unsigned long offset = file->offset;
      21     int block_size = file->device->get_blocksize(file->device->data);
      22
      23     for (i = 0, current = 0;
      24          container->blocks[i].offset != 0; i++)
      25     {
      26             int extent_size = block_size *
      27                               container->blocks[i].count;
      28
      29             if ( (current <= offset) && (offset < current + extent_size) )
      30             {
      31                     return container->blocks[i].offset +
      32                                 (offset - current) / block_size;
      33             }
      34
      35                 current += extent_size;
      36     }
      37
      38     return 0;
      39 }

    file->offset est la position logique dans le fichier. En retour, la fonction est égale au numéro de bloc physique correspondant. La liste des blocs précédemment calculée est stockée dans le tableau contrainer->blocs.
    Le système d’amorçage procédera de la même manière s’il doit charger un RAMDISK. La seule différence notable entre le noyau et le RAMDISK est que le noyau est décompressé par le système d’amorçage alors que le RAMDISK l’est par le noyau.

    Passage de paramètres

    Il y a au moins deux paramètres standards à passer au noyau : la ligne de commande, et, quelquefois, l’adresse du RAMDISK en mémoire. Alors que GRUB utilise une structure de données assez primitive au format multiboot [8], les noyaux pour architecture "m68k" utilisent des tags plus évolués. Pour ceux qui ont connu l’Amiga (une machine de l’architecture m68k) dans les années quatre-vingt, la notion de "tags" leur rappellera étrangement les chunks du format IFF [9]. Le système d’amorçage passe au noyau un pointeur sur le boot record contenant une suite de tags. Chaque tag est identifié par un type. Il contient sa longueur qui permet de trouver le tag suivant et d’ignorer le courant si le noyau ne le connaît pas. Un boot record peut donc contenir la ligne de commande (tag de type V2_BI_COMMAND_LINE) et un autre l’adresse du RAMDISK (tag de type V2_BI_RAMDISK). Vous me direz que c’est bien compliqué pour passer deux informations... effectivement, mais j’ai passé sous silence une phase qui se déroule en début de niveau 2 : l’identification des ressources de la machine. Une des tâches du système d’amorçage est d’identifier le type de la machine (architecture, type de processeur, présence de coprocesseurs) et les ressources présentes (port série, adresse de la mémoire vidéo, organisation de la mémoire vive...). Cette tâche est aisée pour le système d’amorçage, puisqu’il peut encore consulter les routines de la ROM, ce que ne peut pas faire le noyau, une fois démarré. L’emploi des tags permet de passer toutes ces informations d’une manière générique au noyau. Une fois la structure d’information de démarrage initialisée, elle est placée immédiatement après le noyau, ce qui lui permettra de la retrouver.

    Démarrage

    Tout est en place, il ne reste plus qu’à passer la main au noyau en plaçant le pointeur d’instruction du processeur sur son point d’entrée. En fait, ce n’est pas aussi simple : "avant de sortir, éteignez la lumière !", avant de passer la main au noyau, il faut bien veiller à mettre un terme à toutes les activités du système natif, et, plus particulièrement, à ses gestionnaires d’interruptions. Une autre des difficultés spécifiques à cette architecture est la présence et l’utilisation de la MMU du processeur : le système d’amorçage s’exécute avec la mémoire virtuelle activée. Il y a donc translation entre les adresses connues par le système d’amorçage et les adresses réelles de la mémoire physique. Et malheureusement, le noyau doit s’exécuter avec la mémoire virtuelle désactivée. Il faut donc introduire une phase de bootstrap qui va désactiver la mémoire virtuelle et recopier le noyau à l’adresse physique adéquate (que l’on connaît en décodant le format ELF du noyau), puis faire le saut dans le noyau.
    Vous êtes maintenant dans le monde du noyau... et c’est une autre histoire !

    Références
    [1] http://www.gnu.org/software/grub/
    [2] http://lilo.go.dyndns.org/
    [3] http://elilo.sourceforge.net/
    [4] http://yaboot.ozlabs.org/
    [5] http://penguinppc.org/bootloaders/bootx/
    [6] http://emile.sourceforge.net/
    [7] http://www.debian.org/ports/m68k/
    [8] http://www.uruk.org/orig-grub/boot-proposal.html
    [9] http://en.wikipedia.org/wiki/Interchange_File_Format

    Posté par (La rédaction) | Signature : Laurent Vivier | Article paru dans Creative Commons License

    Laissez une réponse

    Vous devez avoir ouvert une session pour écrire un commentaire.


    • Il y a actuellement

    • 668 articles/billets en ligne.