A la découverte d'UNIX (FreeBSD)
Last updated on Tuesday, January 8, 2013
  • 4 semaines
  • Moyen

Ce cours est visible gratuitement en ligne.

Got it!

Recompiler le système

Vous avez sur votre disque dur une version prête à l'emploi du système d'exploitation FreeBSD. Je dis prête à l'emploi, car cette version est écrite en langage binaire, compréhensible par un ordinateur mais pas par un être humain.

Les développeurs de FreeBSD ne l'ont pourtant pas écrit directement dans ce langage : ce sont des humains comme vous et moi (si, si :p !) Ils se sont servi d'un langage de programmation, en l'occurrence le langage C. Il a ensuite fallu traduire ce programme du C au binaire. Et c'est cette traduction, vous le savez, qu'on appelle la compilation.

Le code-source de FreeBSD est disponible sur votre CD-ROM d'installation. Vous l'avez copié sur votre disque dur avec sysinstall. Vous pouvez donc le recompiler.

A - Un peu de stratégie

Image utilisateur

Comme une dragée, UNIX comporte deux parties : le noyau (kernel) et l'espace utilisateur. Chacune doit être recompilée séparément.

Et pourquoi je ferais ça, d'abord ? :colere2: Il est déjà compilé. Pourquoi recommencer ?

C'est vrai que recompiler un code-source est assez long (j'ai compté 1h30 pour le noyau et 3h pour l'espace utilisateur, mais mon ordinateur n'est pas très puissant). Et c'est parfois bruyant >_ , en plus.

Certaines situations peuvent cependant vous y conduire :

  • Pour passer à une nouvelle version de FreeBSD.

  • Si vous modifiez le code-source de l'OS. Vous vous sentez d'attaque pour ça ? :lol:

  • Si vous modifiez la configuration du noyau.

  • Pour préparer l'ouverture de votre première prison.

  • Et bien sûr, le plus important : pour apprendre ! ;)

Tout d'abord, si vous n'avez aucune version du code-source de FreeBSD sur votre disque dur, il est temps d'en installer une : insérez votre CD ou DVD dans son lecteur, lancez sysinstall (en root), choisissez Configure puis Distribution, src et enfin All. Indiquez que vous installez à partir du CD/DVD et, quand c'est fait, quittez sysinstall.

Le code-source de FreeBSD se trouve maintenant dans votre dossier /usr/src. Allez-y mais, avant de nous lancer tête baissée dans la compilation, réfléchissons un peu.

Vous avez l'habitude des compilations make install clean en trois étapes :

  • On construit les fichiers objets temporaires et les exécutables.

  • On installe le programme compilé.

  • On efface les fichiers objets temporaires. Cette fois, pourtant, nous prendrons bien soin d'oublier cette étape : les fichiers objets vont resservir par la suite.

Il faut donc :

  • Construire l'espace utilisateur : make buildworld

  • Installer l'espace utilisateur : make installworld

  • Construire le noyau : make buildkernel

  • Installer le noyau : make installkernel

L'étape make installworld ne doit pas être exécutée sur un système en fonctionnement, au risque de l'endommager. Nous devrons donc passer en mode mono-utilisateur (vous vous souvenez ?)

En attendant, il y a déjà make buildworld.

buildworld ! En voila une commande ! :soleil: Et elle dit bien ce qu'elle veut dire : il ne s'agit pas de compiler un simple programme mais de recréer la totalité de votre espace utilisateur. Bon, c'est sûr, Rome ne s'est pas faite en un jour. Pour le monde, ça va donc vous prendre deux ou trois heures. En attendant, :ange: vous pourrez déjà lire le paragraphe suivant. Mais rien n'interdit d'accélérer un peu le mouvement... ;)

Inutile, par exemple, de recompiler les bibliothèques profilées. Dites-le à make en éditant son fichier de configuration :

[Nom de l'ordinateur]# echo "NO_PROFILE=true" >> /etc/make.conf

Pas besoin non plus d'enregistrer l'heure de chacun des accès aux dossiers /usr/src et /usr/obj (il va y en avoir énormément). Modifiez donc les propriétés de la tranche (partition) /usr :

[Nom de l'ordinateur]# mount -u -o noatime,async /usr

-u indique que vous modifiez les propriétés d'une tranche qui est déjà montée (/usr). -o précède une liste d'options. noatime : ne pas enregistrer l'heure des accès. async : ne pas accéder en écriture toutes les 2 microsecondes (c'est une expression ;) ) mais attendre d'avoir plusieurs informations à écrire.

Enfin, vous pouvez ajouter à make l'option -j4 pour faire travailler davantage votre microprocesseur. Plus le nombre suivant j (jobs) est élevé, plus vous lui envoyez de processus (quasi) simultanément. Mais attention à ne pas confondre vitesse et précipitation ! :o Si vous mettez une valeur trop élevée, vous allez saturer votre microprocesseur et perdre du temps au lieu d'en gagner. Je mets -j4 car j'utilise un processeur mono-coeur sur système réel. Sous VirtualBox, je me serais contenté d'un -j2. Par contre, avec un processeur double-coeur, j'aurais osé -j8.

Vous avez compris ? Alors voici venue l'heure de la création du monde (cet OS est peut-être un peu diabolique :diable: , finalement) :

[Nom de l'ordinateur]# make -j4 buildworld

B - Le code-source

Tandis que le "monde" se construit sous vos yeux ébahis o_O , je vous propose, pour patienter, d'ouvrir une autre console et d'aller faire un tour dans le dossier /usr/src pour jeter un coup d'oeil au code-source que vous êtes en train de compiler. Si vous venez de Windows ou de Mac OS X, ce sera une grande première pour vous. Et si vous venez de Linux, il y a de fortes chances pour que ce soit une grande première quand-même. :p

Image utilisateur

Vous vous souvenez certainement du programme loader, celui qui charge le noyau, et que vous configurez via /boot/loader.conf. Nous allons examiner le fichier principal de son code-source :

% less /usr/src/sys/boot/i386/loader/main.c

Voici donc la version "compréhensible par les êtres humains". Bon alors, évidemment, il faut être bien plus calé en informatique que vous et moi pour comprendre tout ça en détails. Voyons si nous pouvons tout de même y saisir quelque chose.

/*-
 * Copyright (c) 1998 Michael Smith <msmith@freebsd.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

Toutes les lignes qui apparaîssent ici en bleu se situent entre un /* et un */. Ce sont des commentaires. L'ordinateur les ignore. Ils sont là pour qu'un programmeur lisant ce code comprenne tout de suite ce qu'il fait. Les 25 premières lignes sont donc un long commentaire indiquant que la première version de ce programme a été écrite en 1998 par un certain Michael Smith. Vous voyez même son e-mail si vous voulez lui demander des précisions. :D Puis, il y a quelques mentions légales.

#include <sys/cdefs.h>
__FBSDID("$FreeBSD: src/sys/boot/i386/loader/main.c,v 1.44.2.1.2.1 2009/10/25 01:10:29 kensmith Exp $");

/*
 * MD bootstrap main() and assorted miscellaneous
 * commands.
 */

#include <stand.h>
#include <string.h>
#include <machine/bootinfo.h>
#include <machine/psl.h>
#include <sys/reboot.h>

#include "bootstrap.h"
#include "libi386/libi386.h"
#include "btxv86.h"

#define	KARGS_FLAGS_CD		0x1
#define	KARGS_FLAGS_PXE		0x2
#define	KARGS_FLAGS_ZFS		0x4

Les lignes en orange, qui commencent par des #, sont des directives de préprocesseur. Les #include appellent des bibliothèques : des petits bouts de programme dont celui-ci a besoin. Vous voyez par exemple un appel à la bibliothèque <string.h> qui aide à gérer les chaînes de caractères. Celles dont le nom est entouré de < > sont des bibliothèques standards (très répandues et situées dans le dossier /usr/include/). Les autres, dont le nom est entouré de " " sont implémentées dans un autre fichier de ce code-source. Les #define, enfin, fixent la valeur de quelques constantes.

/* Arguments passed in from the boot1/boot2 loader */
static struct 
{
    u_int32_t	howto;
    u_int32_t	bootdev;
    u_int32_t	bootflags;
    union {
	struct {
	    u_int32_t	pxeinfo;
	    u_int32_t	res2;
	};
	uint64_t	zfspool;
    };
    u_int32_t	bootinfo;
} *kargs;

Ensuite, nous trouvons la définition de la structure *kargs et nous apprenons qu'un *kargs est un ensemble de données composé de 4 nombres entiers (de type u_int32_t) qu'on appelle respectivement howto, bootdev, bootflags et bootinfo, ainsi que d'une structure plus petite contenant elle-même deux autres nombres entiers : pxeinfo et res2. Le commentaire au dessus nous indique que tous ces nombres sont des informations transmises à loader par les programmes boot1 et boot2. En effet, vous vous souvenez peut-être (chapitre L'envers du décor) que sont eux qui lancent le programme loader.

static u_int32_t	initial_howto;
static u_int32_t	initial_bootdev;
static struct bootinfo	*initial_bootinfo;

struct arch_switch	archsw;		/* MI/MD interface boundary */

static void		extract_currdev(void);
static int		isa_inb(int port);
static void		isa_outb(int port, int value);
void			exit(int code);

/* from vers.c */
extern	char bootprog_name[], bootprog_rev[], bootprog_date[], bootprog_maker[];

/* XXX debugging */
extern char end[];

static void *heap_top;
static void *heap_bottom;

Puis viennent des déclarations de variables globales, des variables auxquelles toutes les fonctions du programme ont accès. Une fonction est une suite d'instructions à exécuter, instructions qui peuvent dépendre des arguments qu'on donne à la fonction. Elle peut être appelée plusieurs fois dans un programme. Certaines ont un nom commençant par *. Ce sont des pointeurs : elles désignent une certaine case de la mémoire de l'ordinateur.

Il y a aussi quatre prototypes de fonctions (reconnaissables au fait qu'une partie de la ligne est entre parenthèses). Ils indiquent que les fonctions extract_currdev, isa_inb, isa_out et exit seront implémentées plus bas dans le programme. Autrement dit, on y trouvera la suite d'instructions à exécuter quand la fonction est appelée.

int
main(void)
{
    int			i;

    /* Pick up arguments */
    kargs = (void *)__args;
    initial_howto = kargs->howto;
    initial_bootdev = kargs->bootdev;
    initial_bootinfo = kargs->bootinfo ? (struct bootinfo *)PTOV(kargs->bootinfo) : NULL;

Nous en arrivons au coeur du programme : la fonction main. C'est là qu'il va commencer à exécuter des instructions. Les premières consistent à affecter aux variables initial_howto, initial_bootdev et initial_bootinfo les valeurs contenues dans le karg et venant de boot1 et boot2.

/* Initialize the v86 register set to a known-good state. */
    bzero(&v86, sizeof(v86));
    v86.efl = PSL_RESERVED_DEFAULT | PSL_I;

    /* 
     * Initialise the heap as early as possible.  Once this is done, malloc() is usable.
     */
    bios_getmem();

#if defined(LOADER_BZIP2_SUPPORT) || defined(LOADER_FIREWIRE_SUPPORT) || defined(LOADER_GPT_SUPPORT) || defined(LOADER_ZFS_SUPPORT)
    heap_top = PTOV(memtop_copyin);
    memtop_copyin -= 0x300000;
    heap_bottom = PTOV(memtop_copyin);
#else
    heap_top = (void *)bios_basemem;
    heap_bottom = (void *)end;
#endif
    setheap(heap_bottom, heap_top);

    /* 
     * XXX Chicken-and-egg problem; we want to have console output early, but some
     * console attributes may depend on reading from eg. the boot device, which we
     * can't do yet.
     *
     * We can use printf() etc. once this is done.
     * If the previous boot stage has requested a serial console, prefer that.
     */
    bi_setboothowto(initial_howto);
    if (initial_howto & RB_MULTIPLE) {
	if (initial_howto & RB_SERIAL)
	    setenv("console", "comconsole vidconsole", 1);
	else
	    setenv("console", "vidconsole comconsole", 1);
    } else if (initial_howto & RB_SERIAL)
	setenv("console", "comconsole", 1);
    else if (initial_howto & RB_MUTE)
	setenv("console", "nullconsole", 1);
    cons_probe();

    /*
     * Initialise the block cache
     */
    bcache_init(32, 512);	/* 16k cache XXX tune this */

    /*
     * Special handling for PXE and CD booting.
     */
    if (kargs->bootinfo == 0) {
	/*
	 * We only want the PXE disk to try to init itself in the below
	 * walk through devsw if we actually booted off of PXE.
	 */
	if (kargs->bootflags & KARGS_FLAGS_PXE)
	    pxe_enable(kargs->pxeinfo ? PTOV(kargs->pxeinfo) : NULL);
	else if (kargs->bootflags & KARGS_FLAGS_CD)
	    bc_add(initial_bootdev);
    }

    archsw.arch_autoload = i386_autoload;
    archsw.arch_getdev = i386_getdev;
    archsw.arch_copyin = i386_copyin;
    archsw.arch_copyout = i386_copyout;
    archsw.arch_readin = i386_readin;
    archsw.arch_isainb = isa_inb;
    archsw.arch_isaoutb = isa_outb;

    /*
     * March through the device switch probing for things.
     */
    for (i = 0; devsw[i] != NULL; i++)
	if (devsw[i]->dv_init != NULL)
	    (devsw[i]->dv_init)();
    printf("BIOS %dkB/%dkB available memory\n", bios_basemem / 1024, bios_extmem / 1024);
    if (initial_bootinfo != NULL) {
	initial_bootinfo->bi_basemem = bios_basemem / 1024;
	initial_bootinfo->bi_extmem = bios_extmem / 1024;
    }

    /* detect ACPI for future reference */
    biosacpi_detect();

    /* detect SMBIOS for future reference */
    smbios_detect();

    printf("\n");
    printf("%s, Revision %s\n", bootprog_name, bootprog_rev);
    printf("(%s, %s)\n", bootprog_maker, bootprog_date);

    extract_currdev();				/* set $currdev and $loaddev */
    setenv("LINES", "24", 1);			/* optional */
    
    bios_getsmap();

    interact();			/* doesn't return */

    /* if we ever get here, it is an error */
    return (1);
}

Voici (ci-dessus) le reste de la fonction main. Je n'ai pas le temps de vous le détailler (d'autant que le sens de plusieurs de ces lignes m'échappe). Après avoir appris le langage C, vous y reconnaîtrez des tests de conditions (commençant par if), l'affectation de variables d'environnement (avec setenv), une boucle for qui exécute encore et encore certaines instructions et parcourt une à une les cases d'un tableau appelé devsw (pour initialiser les périphériques) jusqu'à en trouver une vide, des appels à la fonction printf pour afficher certaines informations dans la console, et l'instruction finale return qui renvoie 1 en cas de problème.

Ce main fait aussi appel à quelques fonctions qui sont implémentées par la suite. Les voici justement :

/*
 * Set the 'current device' by (if possible) recovering the boot device as 
 * supplied by the initial bootstrap.
 *
 * XXX should be extended for netbooting.
 */
static void
extract_currdev(void)
{
    struct i386_devdesc	new_currdev;
    int			biosdev = -1;

    /* Assume we are booting from a BIOS disk by default */
    new_currdev.d_dev = &biosdisk;

    /* new-style boot loaders such as pxeldr and cdldr */
    if (kargs->bootinfo == 0) {
        if ((kargs->bootflags & KARGS_FLAGS_CD) != 0) {
	    /* we are booting from a CD with cdboot */
	    new_currdev.d_dev = &bioscd;
	    new_currdev.d_unit = bc_bios2unit(initial_bootdev);
	} else if ((kargs->bootflags & KARGS_FLAGS_PXE) != 0) {
	    /* we are booting from pxeldr */
	    new_currdev.d_dev = &pxedisk;
	    new_currdev.d_unit = 0;
	} else {
	    /* we don't know what our boot device is */
	    new_currdev.d_kind.biosdisk.slice = -1;
	    new_currdev.d_kind.biosdisk.partition = 0;
	    biosdev = -1;
	}
    } else if ((initial_bootdev & B_MAGICMASK) != B_DEVMAGIC) {
	/* The passed-in boot device is bad */
	new_currdev.d_kind.biosdisk.slice = -1;
	new_currdev.d_kind.biosdisk.partition = 0;
	biosdev = -1;
    } else {
	new_currdev.d_kind.biosdisk.slice = B_SLICE(initial_bootdev) - 1;
	new_currdev.d_kind.biosdisk.partition = B_PARTITION(initial_bootdev);
	biosdev = initial_bootinfo->bi_bios_dev;

	/*
	 * If we are booted by an old bootstrap, we have to guess at the BIOS
	 * unit number.  We will lose if there is more than one disk type
	 * and we are not booting from the lowest-numbered disk type 
	 * (ie. SCSI when IDE also exists).
	 */
	if ((biosdev == 0) && (B_TYPE(initial_bootdev) != 2))	/* biosdev doesn't match major */
	    biosdev = 0x80 + B_UNIT(initial_bootdev);		/* assume harddisk */
    }
    new_currdev.d_type = new_currdev.d_dev->dv_type;
    
    /*
     * If we are booting off of a BIOS disk and we didn't succeed in determining
     * which one we booted off of, just use disk0: as a reasonable default.
     */
    if ((new_currdev.d_type == biosdisk.dv_type) &&
	((new_currdev.d_unit = bd_bios2unit(biosdev)) == -1)) {
	printf("Can't work out which disk we are booting from.\n"
	       "Guessed BIOS device 0x%x not found by probes, defaulting to disk0:\n", biosdev);
	new_currdev.d_unit = 0;
    }
    env_setenv("currdev", EV_VOLATILE, i386_fmtdev(&new_currdev),
	       i386_setcurrdev, env_nounset);
    env_setenv("loaddev", EV_VOLATILE, i386_fmtdev(&new_currdev), env_noset,
	       env_nounset);

#ifdef LOADER_ZFS_SUPPORT
    /*
     * If we were started from a ZFS-aware boot2, we can work out
     * which ZFS pool we are booting from.
     */
    if (kargs->bootflags & KARGS_FLAGS_ZFS) {
	/*
	 * Dig out the pool guid and convert it to a 'unit number'
	 */
	uint64_t guid;
	int unit;
	char devname[32];
	extern int zfs_guid_to_unit(uint64_t);

	guid = kargs->zfspool;
	unit = zfs_guid_to_unit(guid);
	if (unit >= 0) {
	    sprintf(devname, "zfs%d", unit);
	    setenv("currdev", devname, 1);
	}
    }
#endif
}

La fonction extract_currdev, avec la suite d'instructions à exécuter quand on l'appelle.

COMMAND_SET(reboot, "reboot", "reboot the system", command_reboot);

static int
command_reboot(int argc, char *argv[])
{
    int i;

    for (i = 0; devsw[i] != NULL; ++i)
	if (devsw[i]->dv_cleanup != NULL)
	    (devsw[i]->dv_cleanup)();

    printf("Rebooting...\n");
    delay(1000000);
    __exit(0);
}

/* provide this for panic, as it's not in the startup code */
void
exit(int code)
{
    __exit(code);
}

COMMAND_SET(heap, "heap", "show heap usage", command_heap);

static int
command_heap(int argc, char *argv[])
{
    mallocstats();
    printf("heap base at %p, top at %p, upper limit at %p\n", heap_bottom,
      sbrk(0), heap_top);
    return(CMD_OK);
}

/* ISA bus access functions for PnP, derived from <machine/cpufunc.h> */
static int		
isa_inb(int port)
{
    u_char	data;
    
    if (__builtin_constant_p(port) && 
	(((port) & 0xffff) < 0x100) && 
	((port) < 0x10000)) {
	__asm __volatile("inb %1,%0" : "=a" (data) : "id" ((u_short)(port)));
    } else {
	__asm __volatile("inb %%dx,%0" : "=a" (data) : "d" (port));
    }
    return(data);
}

static void
isa_outb(int port, int value)
{
    u_char	al = value;
    
    if (__builtin_constant_p(port) && 
	(((port) & 0xffff) < 0x100) && 
	((port) < 0x10000)) {
	__asm __volatile("outb %0,%1" : : "a" (al), "id" ((u_short)(port)));
    } else {
        __asm __volatile("outb %0,%%dx" : : "a" (al), "d" (port));
    }
}

D'autres fonctions, implémentées à leur tour.

Le code-source de FreeBSD comporte des centaines de programmes comme celui-ci. Voila pourquoi il est si long de tout recompiler. Encore un peu de patience : il a bientôt fini. :) D'ici là, je vous conseille quand même de quitter la pièce car, en plus d'être un processus long (il paraît qu'il y en a à qui ça prend 7 jours :diable: ), la création du monde est un processus bruyant >_ sur certaines machines.

C - Fin de la recompilation

make buildworld a enfin terminé. :-° C'était de loin l'étape la plus longue. Vous pouvez maintenant vous occuper du noyau :

[Nom de l'ordinateur]# make -j4 buildkernel

Et quand c'est fait (une heure plus tard, environ) :

[Nom de l'ordinateur]# make installkernel

Elle est rapide, celle-là.

Pour la dernière étape, installer l'espace utilisateur, je vous rappelle qu'il faut redémarrer et vous mettre en mode mono-utilisateur (en choisissant 4 dans le menu de boot).

Quand le texte de démarrage cesse de défiler, tapez Entrée pour obtenir le #. Si vous avez bien configuré /etc/ttys, on vous demande votre mot de passe. En le tapant, souvenez-vous que votre clavier est en mode QWERTY.

Si vous essayez immédiatement la commande make installworld, FreeBSD vous répondra qu'il ignore jusqu'au sens du mot make. Vous voyez qu'il n'est pas dans son état normal ! :'(

Il faut l'aider un peu en réactivant les différentes tranches qu'il utilise (celles de type UFS et le swap) :

# mount -u /
# mount -a -t ufs
# swapon -a

La dernière ligne concerne, bien sûr, la tranche swap. / nécessite l'option -u car elle est déjà montée, mais pas correctement. -a signifie qu'on veut pouvoir à la fois lire et écrire sur ces tranches.

Avant de lancer l'installation, il faut encore régler l'horloge interne avec la commande :

# adjkerntz -i

Allez maintenant dans le dossier /usr/src.

Et c'est parti :

# make installworld

Pas de soucis, c'est rapide. :)

Votre nouveau système est prêt. :magicien: Retournez en mode normal.

Example of certificate of achievement
Example of certificate of achievement