Retrouvez cet article dans : Linux Magazine Hors série 25
Pour faire de l’embarqué à peu de frais, la meilleure solution consiste à utiliser une architecture x86 qu’on aura, au préalable, adaptée, aussi bien côté matériel que logiciel. Cet article présente une méthode de construction reposant sur la réutilisation d’une distribution reconnue et très structurée : Debian.
Le présent article poursuit deux objectifs primaires. Premièrement, la construction d’un système embarqué de taille réduite sur carte CompactFlash 16 Mo et, deuxièmement, la compréhension de l’architecture d’un système GNU/Linux.
En effet, au-delà du simple résultat, la construction d’un système devant répondre à un cahier des charges strict est une occasion idéale d’étudier, étape par étape, les éléments qui composent un système GNU. L’utilisation de scripts et de techniques en œuvre dans une distribution comme Debian GNU/Linux nous permettra également de mieux comprendre et apprécier cette distribution pour sa facilité de personnalisation/réutilisation.
1.Matériel
L’architecture de test ici utilisée est délibérément modeste. Il s’agit d’un Pentium 166 MMX utilisé avec une carte mère des plus classiques (pour son époque) et 128 Mo de mémoire. La fréquence du processeur aura été préalablement réduite à 110 Mhz afin de remplacer le couple radiateur/ventilateur par un simple radiateur de cuivre.
Côté périphériques, la carte mère a été joyeusement équipée de tout ce qui traînait dans mes tiroirs : 4 ports USB, 3 ports parallèles, 4 ports série, 2 ports firewire, un adaptateur graphique PCI et une interface Ethernet 10baseT.
L’ensemble constitue quelque chose de très économique et surtout, bien en dessous des configurations x86 dédiées à l’embarqué que l’on trouve dans le commerce. Notez également que la plate-forme de départ pour les expérimentations était un 386/387 équipé de 4 Mo de mémoire. L’émulation 486 du noyau Linux était indispensable pour que l’ensemble soit fonctionnel. Busybox tel qu’empaqueté par Debian ne fonctionnait pas correctement sans émulation. Bien que viable, la configuration était insuffisante en termes de mémoire pour supporter les explications qui vont suivre. Sachez cependant qu’avec un peu d’obstination il est possible de " faire quelque chose qui marche " avec ce type de configuration.

Figure. 1
L’élément le plus important de l’ensemble matériel réside dans l’utilisation de la carte CompactFlash. Il faut, en effet savoir que ce format de carte mémoire dispose d’un mode de compatibilité IDE. En d’autres termes, il est possible de connecter directement une carte CF sur un contrôleur IDE. On remarquera cependant que la carte doit être compatible TrueIDE et pouvoir fonctionner en 5 volts. Les cartes CF ne supportant que 3.3 volts NE DOIVENT PAS ETRE UTILISEES de cette manière. Pour l’anecdote, c’est l’ouverture d’un adaptateur PCMCIA/CF (n’intégrant pas le moindre composant) qui fut à l’origine de l’expérimentation. Les cartes CF disposent également d’un mode de compatibilité PCMCIA permettant une connexion directe.
Il est possible de confectionner un adaptateur " maison " comme celui présenté en figure 1. C’est une expérience intéressante au niveau émotionnel... En particulier lorsqu’on se rend compte que l’amas de fils et de soudures, confectionné d’après des bribes d’informations glanées sur le Web, fonctionne du premier coup et que, ni votre carte CF, ni le contrôleur IDE n’ont été détruits. Cependant, par expérience, je vous recommande l’acquisition d’un adaptateur manufacturé (figure 2).

Figure. 2
Dans tous les cas, une carte CF compatible TrueIDE sera reconnue comme un disque dur par le BIOS du PC (figure 3). La construction du système se fera, très simplement, via l’utilisation d’un lecteur CF USB et les tests seront faits via Bochs.

Figure. 3
En dehors des fonctionnalités en termes de connectivité et de compatibilité, une carte CompactFlash présente les mêmes caractéristiques que l’on retrouve dans tous les matériels de ce type :
- Rapidité. Contrairement à un disque " mécanique ", une carte mémoire n’a pas de période de qualibration des têtes à la mise sous tension. Vous gagnez un temps précieux au démarrage.
- Faible consommation. Une carte CompactFlash consomme quelques 0.5 watt en utilisation contre 2 à 4 watts pour un disque dur standard. Sur une période importante, ceci peut s’avérer non négligeable.
- Aucun bruit. Contrairement à un disque, une carte mémoire ne possède aucune partie mobile mécanisée susceptible de provoquer un bruit (rotation, ventilation, déplacement). En adaptant le reste de la configuration, il vous est ainsi possible d’obtenir un système parfaitement silencieux car parfaitement statique.
- Fiabilité. Cet avantage doit être mitigé. En effet, une carte mémoire est peu sensible aux chocs et aux différences importantes de températures, ce qui est une excellente chose. Cependant on considère généralement qu’un usage intensif d’une mémoire Flash conduit, à moyen terme, à la destruction des unités de mémoire. Hors de question donc de swapper sur de la mémoire Flash. Au contraire, on tâchera de l’utiliser le moins possible. Il n’est pas nécessaire de contrôler la répartition des données sur la carte. La logique interne de cette dernière s’occupe de placer les données de manière à ne pas " user " prématurément une zone plutôt qu’une autre. On notera cependant qu’il est fort possible que les cartes bas de gamme ou d’ancienne génération ne disposent pas de cette fonctionnalité.
En plus de cela, les cartes Compact Flash présentent un dernier intérêt non négligeable : leur faible coût. Ce type de format est encore utilisé pour quelques appareils photo numériques récents (Nikon et Canon), mais de plus en plus de modèles utilisent des cartes à un format plus petit et plus moderne (MMC, SD, Memory Stick ou xD). Ainsi, non seulement leur prix ne cesse de baisser mais on trouve des cartes de faible capacité (16 à 64 Mo) pour une misère sur Internet. Beaucoup d’utilisateurs ont reçu leur appareil photo numérique avec une carte de 16 Mo dont ils n’ont jamais eu l’usage, préférant acheter une carte complémentaire d’une capacité plus importante. Au vu des ventes de ce type d’appareil ces trois dernières années, on se demande si toutes ces cartes ne sont pas tout simplement en train de prendre la poussière dans des tiroirs ou, pire, dans une déchetterie.
2.Première étape : le noyau
La première étape consiste en la préparation du support et la création d’une partition sur la carte CF. Celle-ci occupera l’ensemble du support. L’idée aurait été tentante d’utiliser une carte de 32 Mo partitionnée en deux systèmes de fichier de 16 Mo chacun. Ainsi, il aurait été possible d’avoir un système de secours en cas de défaillance. La sécurité cependant aurait été très limitée et nous pouvons obtenir le même type de comportement avec deux cartes de 16 Mo.
Pour l’heure, nous allons commencer par créer une seule partition sur la carte placée dans un lecteur USB. Celle-ci nous apparaît comme un disque SCSI sous la désignation sda. Voici la partition telle que vue par fdisk (commande p) :
Disk /dev/sda: 16 MB, 16056320 bytes 1 heads, 31 sectors/track, 1011 cylinders Units = cylinders of 31 * 512 = 15872 bytes Device Boot Start End Blocks Id System /dev/sda1 2 1011 15655 83 Linux
On poursuit naturellement avec la création d’un système de fichier standard. Nous parlerons plus loin de l’utilisation d’autres systèmes de fichiers plus adaptés à la mémoire Flash :
% mke2fs /dev/sda1 Type de système d’exploitation: Linux Taille de bloc=1024 (log=0) Taille de fragment=1024 (log=0) 3920 inodes, 15652 blocs 782 blocs (5.00%) réservé pour le super usager Premier bloc de données=1 2 bloc de groupes 8192 blocs par groupe, 8192 fragments par groupe 1960 inodes par groupe Archive du superbloc stockée sur les blocs: 8193 [...]
Une fois le système de fichier créé, nous pouvons nous attaquer au système dans l’ordre chronologique de son futur démarrage. Première étape donc, installer un bootloader performant et souple qui s’occupera de charger notre noyau et de l’exécuter.
Pas de surprise ici, c’est tout naturellement GRUB qui sera utilisé. Nous montons donc le système de fichiers et installons le minimum vital pour GRUB :
% mount /dev/sdb1 /mnt/CF16 % mkdir -p /mnt/CF16/boot/grub % cp /lib/grub/i386-pc/stage1 /mnt/CF16/boot/grub % cp /lib/grub/i386-pc/stage2 /mnt/CF16/boot/grub % cat > /mnt/CF16/boot/grub/menu.lst color white/black black/red timeout 60 default 0 title DeniX embedded Linux 2.4 Pentium MMXUSB root (hd0,0) kernel /boot/vmlinuz-2.4.32-MMXUSB root=/dev/hda1 ^D
Il faut ensuite installer GRUB sur la carte CF. Ceci se fera en deux étapes pour une raison évidente. Le système hôte, le PC sous Debian GNU/Linux disposant du lecteur CF USB, ne possède pas une configuration identique à celle de la cible, ici le Pentium 166.
De la même manière, dans la phase de construction/test, Bochs émulera une machine cible ne disposant que d’un seul disque : la carte CF. La première étape consiste donc à installer le secteur de boot (stage1) sur la carte CF avec des paramètres sans grande importance.
On lance ainsi grub sur le système hôte en utilisant les paramètres correspondants à la configuration actuelle (sur une seule ligne) :
install (hd1,2)/lib/grub/i386-pc/stage1 (hd3) (hd3,0)/boot/grub/stage2 (hd3,0)/boot/grub/menu.lst
Ici, (hd1,2) est la partition racine du système hôte. Nous installons stage1 sur hd3, le quatrième disque du système (la carte CF). On passe en paramètre l’endroit où réside stage2 et le fichier de configuration de GRUB. Nous utilisons ensuite immédiatement l’émulateur Bochs avec la configuration suivante :
megs: 128 romimage: file=/usr/share/bochs/BIOS-bochs-latest, address=0xf0000 vgaromimage: file=/usr/share/vgabios/vgabios.cirrus.bin display_library: x keyboard_type: at keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-fr.map ata0-master: type=disk, path=/dev/sda, cylinders=1011, heads=1, spt=31 boot: c log: bochsout.txt mouse: enabled=0 ips: 5000000
Les lignes importantes sont mises en évidence. On précise respectivement le volume de mémoire disponible, le mappage du clavier (Français X11) et la géométrie du disque telle que rapportée par fdisk. Lorsqu’on lance Bochs, GRUB " ne retrouve pas ses petits ". En effet, (hd3) n’existe pas. On se retrouve alors sur la ligne de commande de GRUB d’où il nous est possible de réinstaller la configuration. Cette souplesse est l’une des raisons pour lesquelles GRUB est tellement apprécié. La commande utilisée est alors (sur une ligne) :
 install (hd0,0)/boot/grub/stage1 (hd0) (hd0,0)/boot/grub/stage2 (hd0,0)/boot/grub/menu.lst
Dès le reset de la machine virtuelle Bochs, GRUB fonctionne correctement et affiche le menu que nous avons spécifié dans menu.lst (figure 4). Bien entendu, pour l’heure nous ne pouvons pas aller plus loin faute de noyau Linux. Nous allons corriger ce point.
La construction du noyau utilisera une méthode " classique " et indépendante de la distribution Debian. Il aurait été possible de créer un paquet Debian via make-kpkg puis d’utiliser son contenu pour peupler les répertoires /boot et /lib/modules. Cela ne présente pas de réel intérêt ici et nous obligerait à installer les sources du noyau via le système APT alors que le système hôte n’en a pas l’usage. De plus, toutes les versions des noyaux Linux ne sont pas disponibles sous forme de paquets.
La construction à proprement parler du noyau n’a rien d’exceptionnel. Après désarchivage des sources, on configurera simplement l’ensemble au strict minimum pour des raisons évidentes d’économie d’espace disque. Le reste est presque instinctif :
% make dep % make bzImage % make modules % make INSTALL_MOD_PATH=/tmp/damodules \ modules_install
Remarquez l’utilisation de la variable INSTALL_MOD_PATH. Celle-ci permet d’installer l’arborescence des modules (lib/modules/2.4*) ailleurs que dans /usr, nous évitant ainsi de " polluer " le système hôte. Ces directives correspondent à un noyau 2.4.x (2.4.32 pour être précis) et devront être adaptées pour un noyau de la série 2.6.

 Figure 4
Note :
GCC 4.0 est maintenant intégré dans presque toutes les distributions. Cependant, la compilation d’un noyau 2.4 avec cette version peut échouer. Pour contourner le problème et ne pas avoir à appliquer de patch sur les sources du noyau, la manière la plus simple est d’utiliser GCC en version 3.3. Il suffit alors de préciser en argument de make le compilateur à utiliser avec HOSTCC=gcc-3.3 et CC=gcc-3.3.
En phase d’expérimentation ou tout simplement pour avoir plusieurs noyaux différents sous la main en cas de besoin, vous pouvez installer plusieurs versions différentes sur la carte CF. Il faudra toutefois prendre garde à ne pas mélanger les modules de ces différentes versions (dans /lib/module/2.4.32 par exemple).
Le noyau est chargé par GRUB et le fichier pourra être nommé arbitrairement. Il n’en va pas de même pour /lib/modules. Pour que le noyau aille chercher ses modules dans un répertoire spécifique, nous utiliserons la variable EXTRAVERSION présente dans le Makefile des sources du noyau. En passant en argument de make, par exemple, EXTRAVERSION=-MMX, le noyau résultant ira chercher les modules automatiquement dans /usr/lib/modules/2.4.32-MMX. On peut ainsi décliner une même version de noyau dans plusieurs configurations différentes sans créer le chaos dans le système. Au besoin, modifiez directement la variable dans les premières lignes du Makefile.
Une fois le noyau et les modules compilés, il ne nous reste plus qu’à copier tout cela sur la carte CF:
% cp arch/i386/boot/bzImage \ / mnt/sda/boot/vmlinuz-2.4.32-MMXUSB % cp -r /tmp/damodules/lib /mnt/sda
Dès lors, un essai dans Bochs nous montre que GRUB charge correctement le noyau en mémoire et lui passe la main. Celui-ci démarre le système et finit par s’arrêter car il ne trouve pas init.
3. Rendre le système utilisable
Le processus de démarrage d’un système GNU/Linux est très simple, du moins durant les premières secondes. Le noyau chargé et exécuté par le bootloader initialise un grand nombre d’éléments matériels et logiciels. Il prend ainsi en charge tout le matériel directement intégré à la configuration comme les contrôleurs IDE, les ports série, etc. Ceci fait, il montera le système de fichier spécifié via le paramètre root= et tentera d’exécuter le premier programme utilisateur du système : init, le processus numéro 1.
3.1 init
Pour l’heure, /sbin/init étant absent, rien ne marche. Deux solutions s’offrent à nous : soit utiliser la version Busybox d’init, soit copier celle du système hôte. Un simple ls -l nous apprend que le binaire ne fait que quelques 30 Ko. C’est suffisant pour nous décider. En effet, cet espace disque consommé vaut largement le principal inconvénient de l’init version Busybox : sa configuration n’est pas standard. Un simple coup d’œil à la page de manuel et le choix est fait. Cette version ne supporte pas le principe des niveaux d’exécution (runlevel).
Nous commençons donc par copier notre cher init dans /usr/sbin de notre carte CF. Bien entendu, tout binaire non compilé en statique possède des dépendances vis-à -vis de bibliothèques. C’est également le cas d’init et cela sera très simplement vérifié avec la commande ldd :
% ldd /sbin/init linux-gate.so.1 => (0xffffe000) libsepol.so.1 => /lib/libsepol.so.1 (0xb7ea9000) libselinux.so.1 => /lib/libselinux.so.1 (0xb7e96000) libc.so.6 => /lib/tls/libc.so.6 (0xb7d5d000) libdl.so.2 => /lib/tls/libdl.so.2 (0xb7d59000) /lib/ld-linux.so.2 (0xb7ef4000)
Ne vous inquiétez pas, je ne vous ferais pas " le coup ldd " à chaque binaire. Non seulement, cela n’apportera rien à l’article mais, de plus, il existe une façon bien plus élégante de copier les bibliothèques du système hôte sur la cible que de lister celles-ci avec ldd et de les copier manuellement. Un script Python nommé mklibs-copy (voire mklibs ou mklibs-small), permet de détecter les dépendances, les trouver sur le système hôte et les copier dans un répertoire spécifié en argument. mklibs-copy va plus loin qu’un simple ldd et vous assure presque à 100% le fonctionnement du binaire sur le système cible. Ici, pour intégrer un init à notre système, nous utiliserons :
% mkdir /mnt/CF16/sbin % cp /sbin/init /mnt/CF16/sbin/ % mklibs-copy -d /mnt/CF16/lib /sbin/init
Le noyau ne se plaint plus de l’absence d’init. C’est une grande réussite. A présent, il se plaint de ne pas pouvoir ouvrir une console. Nous avons avancé d’un pas de lutin. Nous n’avons pas configuré init. Le binaire cherche sa configuration dans /etc/inittab. Celle-ci sera plus complexe qu’avec Busybox, mais nous gagnerons finalement en souplesse en utilisant quelque chose de similaire à un SysVinit Debian.
 Mais, avant toutes autres choses, nous devons créer les points d’entrée dans /dev (source du message à propos de la console). Nous ne pouvons pas utiliser le script MAKEDEV de Debian qui est, c’est un comble, bien trop performant. Non seulement celui-ci ne s’intéresse qu’au /dev du système hôte mais, de plus, il procède à quelques vérifications sur la version du noyau, la présence d’udev, etc. Autant de choses qui nous empêchent de simplement automatiser l’appel à mknod sur /mnt/CF16/dev.
La solution est simple. Nous allons utiliser un MAKEDEV provenant d’ailleurs. Nous récupérons donc la version 1.7 mise à disposition du http://downloads.linuxfromscratch.org/, décompressons le fichier, le rendons exécutable et l’appelons depuis /mnt/CF16/dev :
% mkdir /mnt/CF16/dev % chmod 555 /mnt/CF16/dev % cd /mnt/CF16/dev % /tmp/MAKEDEV-1.7 generic
Nous n’avons maintenant plus à nous préoccuper de /dev et pouvons passer à la suite. Nous commençons donc par créer le fichier de configuration principal d’init (/etc/inittab) :
id:2:initdefault: si::sysinit:/etc/init.d/rcS l0:0:wait:/etc/init.d/rc 0 l1:1:wait:/etc/init.d/rc 1 l2:2:wait:/etc/init.d/rc 2 l3:3:wait:/etc/init.d/rc 3 l4:4:wait:/etc/init.d/rc 4 l5:5:wait:/etc/init.d/rc 5 l6:6:wait:/etc/init.d/rc 6
Dans l’ordre nous :
- spécifions le niveau d’exécution par défaut ;
- précisons le script systématiquement utilisé au moment du boot, le script d’initialisation du système ;
- pour chaque niveau d’exécution, précisons un argument différent pour un script unique.
Le contenu du ficher /etc/init.d/rcS est très simple :
#! /bin/sh exec /etc/init.d/rc S
Nous retrouvons le fameux script rc avec l’argument S en lieu et place d’une valeur numérique. Tout le fonctionnement de la configuration d’init repose donc sur le script rc que nous allons, tout simplement, copier du système hôte. Nous avons donc, à ce stade :
% mkdir /mnt/CF16/etc/init.d % mkdir /mnt/CF16/etc/default % cp /etc/init.d/rc /mnt/CF16/etc/init.d % cp /etc/default/rcS /mnt/CF16/etc/default % cat > /mnt/CF16/etc/init.d/rcS #! /bin/sh exec /etc/init.d/rc S ^D % chmod +x /mnt/CF16/etc/init.d/rcS
Puis, nous n’oublions pas de copier le shell Bash et l’utilitaire stty :
% mkdir /mnt/CF16/bin % cp /bin/stty /mnt/CF16/bin % cp /bin/bash /mnt/CF16/bin/sh % mklibs-copy -d /mnt/CF16/lib \ /bin/stty /bin/bash
Résumons les faits. A ce stade, init utilisera son inittab pour exécuter le script rc avec l’argument S puis le même script avec l’argument 2. Ce script s’occupera de lancer d’autres scripts en cas de changement de niveau d’exécution. Pour déterminer quel script lancer et avec quel argument, il utilisera l’architecture suivante :
- Le répertoire
/etc/init.dcontient tous les scripts. - Des répertoires correspondant aux niveaux d’exécution (
/etc/rc0.d,/etc/rc1.d.../etc/rcS.d) contiennent des liens symboliques vers les scripts correspondants dans/etc/init.d. - Le nommage des liens symboliques détermine ce que le script
rcdoit en faire. Le premier caractère du nom définit l’action. Un " S " demandera le lancement du script dans/etc/init.davecstarten argument. Un " K " fera de même avecstopen argument. Le numéro spécifié entre la première lettre du nom et le nom du script détermine l’ordre d’exécution. Nous pourrons avoir par exemple,/etc/rc.1/S50totocorrespondant Ã/etc/init.d/toto starttel que lancé parrc.
Pour l’heure, rien n’est en état de fonctionner sur notre système cible. Il nous faut créer les scripts de démarrage du système.
3.2 Les premiers scripts d’init
Nous allons faire simple. Les scripts suivants n’utilisent pas le squelette classique Debian consistant à tester le paramètre en argument. Ils pourront être facilement adaptés par la suite. Le principal but ici est d’arriver à obtenir rapidement un système évolutif et fonctionnel.
Pour que les scripts soient faciles à écrire, ils ne peuvent pas reposer uniquement sur le shell. Nous avons besoin d’un certain nombre de commandes qui nous seront fournies par Busybox. L’installation est, une fois encore, des plus simples. Après installation du paquet busybox sur le système hôte, nous procédons comme suit :
% cp /bin/busybox /mnt/CF16/bin % mklibs-copy -d /mnt/CF16/lib /bin/busybox
L’étape suivante consiste à créer tous les liens symboliques pointant vers le binaire busybox. Pour cela, le plus simple est de récupérer la liste des fonctions dans un fichier avec busybox > /tmp/liste et de formater l’ensemble de manière à obtenir la liste avec une commande par ligne.
On veillera à supprimer sh de la liste pour conserver notre Bash ainsi que busybox pour ne pas écraser le binaire. Une petite boucle suffit ensuite à créer tous les liens :
% /mnt/CF16/bin
% for i in `cat /tmp/liste`;
do /bin/ln -s busybox $i;
done

Figure. 5
Nous pouvons maintenant nous attaquer à la partie la plus intéressante. Voici les quelques scripts dont nous aurons besoin :
mountall: Suite au démarrage du noyau, le système de fichiers racine est monté. C’est ce qui permet le démarrage d’init. Cependant, il l’est en lecture seule et nous devons le remonter en lecture/écriture. Parallèlement, pour que le système soit utilisable, il nous faut monter le pseudo système de fichier/proc :
#!/bin/sh PATH=/sbin:/bin echo „Remounting FS rw“ /bin/mount -w -n -oremount / echo „Deleting mtab“ /bin/rm -f /etc/mtab echo „Mounting PROCFS“ /bin/mount /proc
Nous créons /etc/rcS.d et plaçons un lien symbolique S10mountall pointant sur /etc/init.d/mountall. Remarquez que le fichier pointé n’existe pas sur le système hôte, mais sera parfaitement valide sur le système cible démarré. N’oubliez pas de créer /proc.
Puisque nous en sommes au montage des systèmes de fichiers, profitons-en pour ajouter un /etc/fstab correct à la cible :
/dev/hda1 / ext2 errors=remount-ro 0 1 proc /proc proc defaults 0 0
keyshellest le script qui va nous permettre de " parler " avec le système :
#!/bin/sh PATH=/sbin:/bin echo „Loading Keyboard“ loadkeys /etc/boottime.kmap.gz HOME=/root USER=root export HOME USER cd $HOME echo „Starting Shell“ /bin/sh
Comme nous n’avons pas l’intention de nous compliquer la vie, nous chargeons un mappage français pour le clavier. Nous récupérons tout simplement le fichier correspondant sur le système hôte (/etc/console/boottime.kmap.gz) et le copions dans /etc sur système cible. Si votre Debian est correctement configurée, tout est parfait. Dans le cas contraire, install-keymap est votre ami. Pour charger le clavier, nous avons besoin de loadkeys. Nous copions le binaire depuis /bin et n’oublions pas de copier les bibliothèques avec mklibs-copy. Créons les répertoires nécessaires et ajoutons le lien :
% cd /mnt/CF16/etc % mkdir rc0.d rc1.d rc2.d rc3.d \ rc4.d rc5.d rc6.d ../root % cd rc2.d % ln -s /etc/init.d/keyshell S20keyshell
umount.Pour que le système s’arrête proprement, nous devons démonter les systèmes de fichiers et remonter, en lecture seule, ceux en cours d’utilisation. Le script est très simple :
#! /bin/sh PATH=/sbin:/bin echo „Umounting ALL“ umount -f -a -r echo „Remouting / RO“ mount -n -o remount,ro /
Cette fois, le lien symbolique sera dans /etc/rc0.d et se nommera S60umount.
haltest encore plus simple que le script précédent puisqu’il ne fait qu’afficher un message. En fonction du matériel, il sera possible de provoquer un véritable arrêt du système en exécutant un binaire pilotant l’alimentation (ATX ou via un montage à relais). Pour l’heure, nous nous en tenons au simple message :
#! /bin/sh PATH=/sbin:/bin echo „SYSTEM DOWN.“
Le lien symbolique trouve sa place dans rc0.d et s’appellera S90halt afin d’être lancé après S60umount.
4.Devenir accessible
4.1 Support réseau
Notre plate-forme d’expérimentation dispose d’une carte compatible NE2000 sur bus ISA. Cela tombe plutôt bien puisque Bochs est en mesure d’émuler ce type de carte.
Bien entendu, les explications qui vont suivre sont, pour la plupart, valables pour tous types d’interfaces réseau supportées par le noyau. Les expérimentations se feront via l’émulation NE2000, puis il suffira de changer le module à charger au démarrage.
Pour ajouter une interface NE2000 dans la configuration de Bochs, il suffit d’ajouter dans votre .bochsrc une ligne comme (nous la réviserons par la suite) :
ne2k: ioaddr=0x340, irq=9, mac=b0:c4:20:00:00:00, ethmod=null
Le reste se passe dans la configuration du système cible. Plutôt que de placer directement le chargement du module dans un script, nous allons utiliser, comme avec le système hôte, un fichier /etc/modules. Notre script d’init ressemblera donc à ceci :
#! /bin/sh
PATH=/sbin:/bin
echo „Loading modules:“
(cat /etc/modules; echo) | # add LF
while read module args
do
case "$module" in
\#*|"") continue ;;
esac
echo "$module ($args)"
modprobe $module $args
done
Nous ajoutons dans /etc/rcS.d un lien S12modules pointant vers /etc/init.d/modules. Je pars ici du principe que le support de l’interface réseau, au niveau matériel, est intéressant pour tous les niveaux d’exécution, d’où le lancement du script depuis rcS.d. Il ne nous reste plus, ensuite, qu’à ajouter notre fichier listant les modules à charger, contenant une simple ligne ne io=0x340. Nous ajoutons également les binaires permettant de gérer les modules du noyau :
% cp modprobe insmod insmod.modutils \ rmmod /mnt/CF16/sbin/ % ln -s insmod.modutils modprobe.modutils % ln -s insmod.modutils rmmod.modutils
Les versions .modutils sont ici par souci de compatibilité. En effet, le noyau du système cible étant un 2.4 nous profitons largement de cette astuce puisque les commandes sont extraites d’un système basé sur un 2.6. Nous pourrons ensuite basculer vers un 2.6 avec un minimum de modifications sur la cible.
A présent que l’interface est prise en charge, il nous reste à la configurer. On trouve habituellement dans les systèmes embarqués des scripts de configuration souvent trop rigides utilisant ifconfig et route directement. Ici, nous allons piocher dans le système hôte pour arriver à une solution légère et efficace.
Commençons donc par créer l’arborescence nécessaire et typique de Debian sur le système cible :
% mkdir /mnt/CF16/etc/network % cd /mnt/CF16/etc/network % mkdir if-down.d if-post-down.d \ if-pre-up.d if-up.d
Il ne reste ensuite qu’à créer le fichier de configuration classique /etc/network/interfaces contenant, par exemple :
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 192.168.1.122
netmask 255.255.255.0
network 192.168.1.0
broadcast 192.168.1.255
gateway 192.168.1.10
Ce fichier de configuration est utilisé par les commandes ifup et ifdown. Il convient donc de les copier sur le système cible, dans le répertoire /sbin. Ce n’est qu’alors que nous pouvons nous pencher sur le script d’init, /etc/init.d/networking. Celui-ci reposera sur le modèle des scripts Debian (en version simplifiée) :
#! /bin/sh
PATH=/sbin:/bin
case „$1“ in
start)
echo „Networking start“
ifup -a
;;
stop)
echo „Networking stop“
ifdown -a
;;
*)
echo „Usage: /etc/init.d/network {start|stop}“
exit 1
;;
esac
Cette fois, nous ajouterons, dans rc2.d, un lien pointant sur le script que nous venons d’ajouter, appelé S15networking permettant de configurer l’interface avant le lancement d’un shell. Un second lien sera placé dans rc0.d sous le nom K09networking permettant de " déconfigurer " l’interface. Avant démarrage de Bochs, nous créons /etc/network/run dont ifup et ifdown ont besoin pour gérer le fichier ifstate indiquant l’état des interfaces.
Vous pouvez d’ores et déjà lancer Bochs pour constater le bon fonctionnement des scripts et des commandes utilisées. Un ifconfig une fois le système cible démarré devrait correctement lister les interfaces lo et eth0.
Mais nous devons correctement configurer Bochs afin d’obtenir un fonctionnement réseau réellement fonctionnel. Il existe plusieurs manières de procéder. Je choisirai ici la plus classique et celle permettant de faire le plus de tests par la suite : TUN/TAP. Le principe consiste à créer une nouvelle interface virtuelle sur l’hôte qui sera reliée à l’interface émulée par Bochs. Avant toutes choses, chargez le module tun (en particulier sur vous utilisez udev).
Nous modifions ensuite la configuration présente dans le .bochsrc comme suit (sur une ligne) :
e2k: ioaddr=0x340, irq=9, mac=b0:c4:20:00:00:00, ethmod=tuntap, ethdev=/dev/net/tun, script=/mnt/six2/tunconfig
La partie importante réside dans le script de configuration de l’interface virtuelle /mnt/six2/tunconfig :
 #!/bin/bash
/sbin/ifconfig ${1##/*/} 192.168.1.10
Ceci nous permettra de créer automatiquement l’interface via le démarrage de Bochs. Dernière étape pour fournir à notre système cible un accès vers le reste du monde, nous mettons en place un masquerading avec iptables :
% iptables -t nat -A POSTROUTING \ -s 192.168.1.0/24 -d ! 192.168.1.0/24 \ -j MASQUERADE % sysctl -w net.ipv4.ip_forward=1
Dès lors, notre système cible peut pinguer l’hôte et le reste du monde, mais on peut également accéder au système cible depuis l’hôte. Ceci nous permet de passer à la suite.
4.2 Installation d2e services
Le shell lancé en fin d’initialisation n’est pas une solution définitive. En effet, cela n’est utile que pour les premiers tests, mais dans un environnement de production, il conviendra de lancer un getty appelant login qui, après authentification, lancera un shell. Une telle infrastructure risque d’occuper de la place pour rien sur un système embarqué. La plate-forme n’est pas destinée à s’équiper d’une console (écran/clavier). On préfèrera habituellement un accès via le réseau ou un port série dédié à cet usage.
Voilà donc l’occasion rêvée de pousser plus loin la configuration. Nous allons ajouter un accès Telnet à notre système cible. Pour ce faire, nous utiliserons le super serveur Xinetd en lieu et place de l’habituel Inetd. Comme précédemment, nous commençons par utiliser cp et mklibs-copy pour ajouter le binaire xinetd dans /mnt/CF16/sbin et les bibliothèques dans /mnt/CF16/lib.
Xinetd utilise une configuration stockée dans /etc/xinetd.conf pour tout ce qui est global et parcourt ensuite (via une ligne dans la configuration) le répertoire /etc/xinetd.d à la recherche de fichiers de configuration spécifiques aux services.
Nous créons donc le fichier de configuration principal contenant simplement :
defaults
{
}
includedir /etc/xinetd.d
Dans le répertoire /etc/xinetd.d, nous ajoutons un fichier daytime permettant de faire les premiers tests. Celui-ci contiendra :
 service daytime
{
disable = no
type = INTERNAL
id = daytime-stream
socket_type = stream
protocol = tcp
user = 0
wait = no
}
Enfin, nous pouvons créer notre script d’init. Nous gèrerons les arguments start et stop et nous lancerons le serveur via start-stop-daemon (à copier sur le système cible) :
#! /bin/sh
PATH=/sbin:/bin
case „$1“ in
start)
echo „Superserver start“
start-stop-daemon --start --quiet \
--exec /sbin/xinetd \
-- -pidfile /var/run/xinetd.pi
;;
stop)
echo „Superserver stop“
start-stop-daemon --stop --quiet --oknodo \
--pidfile /var/run/xinetd.pid \
--exec /sbin/xinetd
;;
*)
echo „Usage: /etc/init.d/inetd {start|stop}“
exit 1
;;
esac
Pour compléter l’ensemble, nous créons /var/run sur le système cible. start-stop-daemon nous permet de lancer des processus " démons " de manière unique et simplifiée. Pour le démarrage du super serveur, nous demandons à xinetd lui-même de créer un fichier contenant son PID. Pour arrêter le service, start-stop-daemon réutilisera le contenu de ce fichier pour tuer le processus.
Nous ajoutons également un certain nombre de fichiers classiques pour les systèmes GNU/Linux. Certains sont adaptés en fonction de la simplicité du système cible :
/etc/hosts/etc/passwd(permissions 644)/etc/passwd-(permissions 600)/etc/shadow(permissions 640)/etc/shadow-(permissions 600)/etc/group(permissions 644)/etc/group-(permissions 600)/etc/hostname
On ajoutera à cette occasion la ligne hostname -F /etc/hostname dans le script /etc/init.d/networking, juste après l’appel à ifup. D’autres fichiers sont simplement copiés depuis le système hôte :
- /etc/rpc
- /etc/services
- /etc/protocols
- /etc/nsswitch.conf
Ce dernier fichier est important. Il s’agit de la configuration utilisée par le système NSS (Name Service Switch) GNU. Ce fichier est nécessaire, tout autant que les bibliothèques /lib/libnss* qui devront être copiées sur le système cible. Ces bibliothèques contiennent les fonctions permettant la lecture des différentes sources de configuration. La fonction getprotobyname de la libc GNU par exemple, semble lire directement /etc/protocols (dixit la manpage) mais, fait en réalité appel à NSS (chose absolument invisible pour mklibs-copy). NSS est d’ailleurs un sujet qui mériterait un article complet par ailleurs.
Il ne reste maintenant plus qu’à créer les liens permettant le démarrage et l’arrêt automatique du super serveur : S17xinetd dans rc2.d et K09networking dans rcS.d. Le système est maintenant prêt pour un test simpliste : une connexion Telnet sur le port 13 depuis le système hôte :
% telnet 192.168.1.122 13 Trying 192.168.1.122... Connected to 192.168.1.122. Escape character is ‘^]’. 28 MAR 2006 15:29:19 UTC Connection closed by foreign host.
Embrayons joyeusement sur la suite en ajoutant Telnet avec le duo cp/mklibs-copy. Le binaire utilisé est celui fourni par le paquet inetutils-telnetd. Nous ajoutons un fichier telnet dans le /etc/xinetd.d de la cible :
service telnet
{
disable = no
protocol = tcp
socket_type = stream
user = 0
wait = no
server = /sbin/telnetd
}
Nous n’avons presque plus besoin de notre shell lancé depuis le script keyshell. A présent, le système est accessible depuis le port 22. A ce stade, nous avons un système fonctionnel minimal communicant, le tout pour environ 5.2 Mo. Il reste presque les deux tiers de la capacité de la carte CF pour les applications.
4.3 Une console série
Lorsqu’on parle d’embarqué, il est rare de prévoir écran et clavier à proximité du système. Ici, bien qu’il s’agisse d’une carte mère PC standard, et donc d’une taille conséquente, nous souhaitons également nous passer de ces périphériques. La liaison Telnet offre déjà un accès au système cible, mais un problème réseau est vite arrivé. Il nous faut une solution de secours.
Pas de miracle ou d’extravagance ici, la solution la plus simple est d’utiliser l’un des ports série de la future machine cible en guise de console. Ce choix est parfaitement justifié, mais pas innocent. En effet, l’émulation d’un port série par Bochs fait partie de l’aspect didactique de cette section. A l’instar de l’émulation d’une interface réseau, il existe plusieurs méthodes permettant de donner accès à un port série. Nous allons utiliser ici un TTY (PTS) en guise de port ou plus exactement en guise d’un des côtés d’un pseudo câble null-modem.
Pour ce faire, ouvrez un terminal (Xterm ou autre) non loin de celui d’où vous lancez Bochs et identifiez le TTY avec la commande, bien nommée, tty. Ici, se sera /dev/pts/4. Dans la configuration de Bochs, ajoutez une ligne :
com1: enabled=1, mode=term, dev=/dev/pts/4
Dès le prochain démarrage de Bochs, tout ce qui part depuis le système cible sur le premier port série (ttyS0 sous GNU/Linux) arrivera dans le terminal identifié par /dev/pts/4. On prendra soin de lancer une commande type sleep 500000 sur le terminal en question pour éviter que le shell ne s’en mêle.
Côté système cible, nous n’avons plus qu’à copier getty dans /sbin, puis à modifier notre /etc/inittab en ajoutant :
T0:23:respawn:/sbin/getty 38400 ttyS0 linux
Ceci aura pour effet d’être à l’écoute des connexions sur ttyS0 et, au besoin, de lancer login gracieusement fourni par Busybox. Puisque nous en sommes à ajouter getty, profitons-en pour nous débarrasser de l’appel au shell depuis un script d’init et ajoutons :
1:2345:respawn:/sbin/getty 38400 tty1
Dans /etc/init.d/keyshell, nous ne conservons que le chargement du mappage clavier avec loadkeys. Nous obtenons ainsi un système parfaitement " normal ".
Conclusion : Encore du travail...
Tout cela constitue une simple base de travail. Beaucoup de choses peuvent ou doivent être ajoutées. Voici quelques exemples :
- Standardiser les scripts d’
init. - Le démarrage/montage/démontage du système est pour l’instant géré de manière barbare. Un certain nombre de vérifications et de nettoyages (
ifstateouxinetd.pid) sont nécessaires après un arrêt brutal du système. - Étant donné le volume de mémoire à notre disposition, il est possible d’envisager d’utiliser un système de fichiers tmpfs pour
/tmpet/var. - En fonction du type d’application visé, il pourra être nécessaire d’installer un
syslogousyslog-ng. - Un serveur Web à faible empreinte mémoire comme Boa ou Thttpd pourrait faire office d’interface utilisateur.
- Il est possible d’étendre le système ou de le cloner, de manière à ce que GRUB, en cas de problème puisse démarrer sur une autre carte CF.
- Un système de watchdog pourra être ajouté, permettant, en cas de blocage, de redémarrer le système.
Ensuite, l’étendue des possibilités n’est limitée que par votre imagination et le matériel à votre disposition. Notez que la construction d’un système comme celui-ci est également possible pour une cible non-x86. Cela nécessitera la recompilation des binaires, mais vous apportera la maîtrise de l’ensemble. C’est une question de temps. Pour conclure, je tiens à remercier Pierre Ficheux pour les explications données dans son livre Linux embarqué et basées sur la distribution Red Hat. Celles-ci ont été le déclencheur des premières expérimentations sous le motif, certes douteux, de " faire pareil avec Debian ".
Enfin, je signalerais que deux images sont disponibles dès à présent sur http://www.gnulinuxmag.com/pub/LM_HS/HS25/sda.img.bz2 contient l’intégralité des données de la carte CF (16 Mo) et sda1.img.bz2 est l’image de la partition qu’il est possible de directement monter. Libre à vous de les réutiliser comme base de travail ou pour simplement vérifier des points restés obscurs dans les explications précédentes.
 Retrouvez cet article dans : Linux Magazine Hors série 25

