Houyhnhnms,

Un noyau et une distribution Linux pour le NCD Explora 401


Par Mathieu Avila. Je suis à la recherche d'un emploi dans le domaine du Linux Embarqué, voir ici.


(Cliquez pour agrandir)

Table des matières


  

Introduction

    Gulliver, le Linux User's Group (LUG) de Rennes et ses environs, a reçu sous forme de don un certain nombre de machines Explora 401 de type "NCD", pour Network Computer Device, machines de petites capacités dont l'unique fonctionnalité est d'embarquer un serveur X pour faire de l'affichage déporté d'application tournant sur un serveur disposant, lui, de grosses capacités.
    Ces machines sont fournies avec un système d'exploitation (ou OS, Operating System) propriétaires, qui est téléchargé par le NCD chaque fois qu'il doit démarrer. Elles n'embarquent donc pas de disque dur, mais seulement 16Mo de RAM, plus une carte réseau, une carte graphique, 2 connecteurs PS/2 clavier et souris, un port série, un port PCMCIA et enfin un processeur PowerPC 403GA.
    Le but du LUG est de mettre en avant des logiciels libres, il n'existait malheureusement pas d'OS libre, tel que Linux, capable de fonctionner avec, il se retrouvait avec des machines inutilisables. J'ai donc entrepris de réaliser un OS à base du noyau Linux, capable de fonctionner sur cette machine.
    Le nom "Houyhnhnms" vient des aventures de Gulliver, le héros de Jonathan Swift, et correspond à une des terres découvertes par Gulliver. Il est particulièrement imprononçable, ce qui en fait un nom parfait. ;)
    Ce document décrit le portage du noyau Linux sur les machines de type Explora 401, et est réalisé dans le but d'aider quiconque souhaiterait se lancer dans une aventure de ce type. Ce document ne se veut pas parfait : si vous y voyez des erreurs, n'hésitez pas à m'en faire part.

    Note importante :  Une grande partie du travail a consisté à adapter ce qui a déjà été fait pour l'architecture PowerPC, voire même à casser pour enlever des morceaux. Les explications données ici concernent une partie du travail que je n'ai pas réalisé, mais qu'il est utile ou intéressant de détailler. Je ne prétends pas avoir écrit le code PPC du noyau !!

Description

    Il y a 2 niveaux de portage pour un noyau d'un système d'exploitation tel que Linux : le portage pour le processeur proprement dit, mais également pour ce qui tourne autour, l'architecture. Par chance, le noyau a été porté pour une machine du même type, l'Explora451 disposant d'un PPC40GCX. La tache était donc déjà plus facile.

    Cependant, le processeur PPC403GA a une particularité importante qui le distingue de son frère le 403GCX : contrairement à la plupart des processeurs récents, il n'embarque pas de module de gestion mémoire (MMU, pour Memory Management Unit) permettant d'isoler les programmes les uns des autres, de leur fournir un espace d'adressage particulier, etc. Un "patch" intégré au noyau linux depuis la version 2.6, nommé uClinux, est justement fait pour ce type de processeur, mais n'a jamais été fabriqué pour fonctionner sur des machines PowerPC. Son adaptation est donc nécessaire. Les programmes utilisant ce patch ont également un format particulier, généré par un programme qui n'a pas été non-plus fait pour fonctionner avec les PowerPC. Là aussi, une adaptation est nécessaire.

    Un noyau Linux est complètement inexploitable s'il est tout seul. Il est en général accompagné au moins d'une bibliothèque C apportant les fonctions de base à tout programme C, et d'un ensemble de programmes, mais également de tout ce qu'il faut pour développer de nouvelles applications pour cette machine. Voici donc ce qui compose exactement le projet :

Chaine de compilation

La chaine de compilation se décompose en:

Pour construire les binutils :

wget http://ftp.gnu.org/gnu/binutils/binutils-2.14.tar.gz
tar -xvzf binutils-2.14
cd binutils-2.14
./configure --prefix=/PATH/TO/TARGET/DIR/ --target=powerpc-elf-linux

make all
make install

Cela permet d'obtenir les outils installés dans  /PATH/TO/TARGET/DIR/
Pour construire le compilateur C, il est nécessaire de référencer les binutils fraichement compilés sur lesquels la compilation s'appuie.

export PATH=$PATH:/PATH/TO/TARGET/DIR/bin/
tar -xvzf gcc-3.3.2.tar.gz
cd gcc-3.3.2
./configure --with-gcc-version-trigger=/data/avila/TOOLCHAIN_PPC/gcc-3.3.2/gcc/version.c \
--prefix=/PATH/TO/TARGET/DIR/ \
--target=powerpc-elf-linux --enable-languages=c \
--disable-threads \
--disable-shared \
make all install
Tout ceci permet effectivement de construire un compilateur C, mais le lien ne sera pas fait avec les bibliothèques C, et la construction échouera quelque part au milieu. Ce qui n'est pas grave, car le noyau n'a pas besoin des bibliothèques C. En effet, il doit contenir toutes les routines C dont il a besoin sans avoir à faire appel à des fichiers externes, notion définie par le noyau lui-même.

La compilation des programmes utilisateurs, plus loin, nécessitera elle de compiler une bibilothèque C, uClibc.

Noyau Linux

Description

Le noyau Linux est un objet devenu extrêmement complexe à cause de la multiplicité des architectures prises en charge et les possiblités qu'il offre; néanmoins il est très facile à explorer, et de l'adapter, car son architecture est particulièrement efficace. Le plan adopté pour cette section devrait permettre de rentrer progressivement dans les détails.
Il est tout d'abord question de la compilation d'une architecture à ajouter. Puis le découpage du boot du noyau en 1er et 2nd étage. Le 2nd étage est le plus touffu. Il décrit notamment le processus de boot, le timer, l'allocation mémoire, l'adaptation des fonctions binfmt_flat, le fork.

Insertion d'une nouvelle architecture

Création des répertoires

Petit schéma rapide des répertoires des sources du noyau:
+ crypto                                    La bibliothèque qui contient les routines de cryptographie
+ Documentation Surement la Documentation, mais le code se suffit à lui-même
+ drivers Tous les pilotes de périphériques, classés par catégories
+ video
+ usb
+ serial
+ char
+ ...
+ fs L'épine dorsale de la gestion des fichiers, le VFS (virtual filesystem), la gestion des formats des programmes
+ ext2 Le système de fichier EXT2
+ ext3 Le système de fichier journalisé EXT3, basé sur EXT2
+ romfs Un système de fichier statique, lecture seule.
+ jffs Le système de fichier le plus employé dans l'embarqué
+ ... Tous les sytèmes de fichiers exotiques, ou provenant de systèmes tiers.
+ include Tous les fichiers d'en-têtes
+ asm-m68k Les en-têtes spécifiques à l'architecture 68000, plus quelques inline en assembleur.
+ asm-m68knommu Les en-têtes spécifiques à l'architecture 68000 sans MMU, plus quelques inline en assembleur.
+ asm-ppc Les en-têtes spécifiques à l'architecture PowerPC, plus quelques inline en assembleur.
+ asm Un lien symbolique vers l'architecture sélectionnée en cours de compilation.
+ config Les fichiers de configuration,
+ linux Les en-têtes spécifiques au coeur du noyau, (pour /kernel, /lib, ...)
+ ... Les en-têtes de fichiers des sous-systèmes (sound, pcmcia,...)
+ init Les fichier de boot du noyau
+ ipc Les fichiers relatifs aux communications inter-processus.
+ kernel Les fichiers principaux de la gestion des processus.
+ lib Bibliothèque contenant des routines génériques, ainsi que la compression ZLIB.
+ mm La gestion mémoire (memory management) dans le noyau
+ net La gestion du réseau (network)
+ usr Une partie qui se retrouvera côté utilisateur (hors-noyau).
+ arch Toutes les spécificités de chaque architecture supportée.
+ m68k Architecture Motorola 68000
+ m68knommu Architecture Motorola 68000 ne disposant pas de MMU
+ ppc Architecture PowerPC
+ kernel Les procédures appelées du noyau et spécifiques à l'architecture PowerPC
+ mm Procédures de gestion mémoire spécifiques à l'architecture PowerPC
+ 8xx_io Gestion des I/O pour les processeurs PowerPC 8xx
+ 4xx_io Gestion des I/O pour les processeurs PowerPC 4xx
+ configs Des fichiers .config prédéfinis pour chaque sous-architecture
+ boot Code lié aux procédures de boot des machines PPC
+ amiga Spécifique aux machines Amiga
+ platforms Les routines spécifiques à chaque plate-forme. Quasiment 1 fichier par plate-forme supportée.
+ 4xx Spécificités des processeurs PowerPC 4xx, comme celui de l'Explora
+ oprofile
+ xmon Monitorer le PowerMac
+ syslib Des routines utiles pour le boot, les io, dma, ... (le nom du répertoire n'est pas très heureux)
+ 8260_io
 
+ ...
+ script Des scripts de compilation, configuration, etc.
+ security Ce qui concerne la sécurité
+ sound Le son (alsa, OSS)

Pour insérer une nouvelle architecture, il est nécessaire de ranger proprement ses affaires, pour ne pas "polluer" l'existant.

On insère donc naturellement:

Pour faire simple et limiter le temps de développement, on repart de ce qui a été fait pour l'architecture PowerPC, en copiant arch/mmu vers arch/nommu et include/ppc vers include/ppcnommu. Les liens sont modifiés, ainsi que les références absolues comme par exemple celle à  "arch/ppc".

Création de l'architecture dans la compilation

Encore une fois, Linux est particulièrement efficace en ce qui concerne les architectures multiples. La configuration est gérée par des fichiers "Kconfig" (rien à voir avec KDE), qui contiennent toutes les définitions d'options, leur type (n-choix, booléen, chaine), leurs contraintes (on ne compile pas un module de carte son si le son n'est pas activé), une aide contextuelle associée, et un rangement de ces options en catégories et sous-catégories.

Chaque architecture possède ainsi un fichier Kconfig, qui inclut à son tour les fichiers Kconfig standard du noyau à sa guise : sound/Kconfig, fs/Kconfig, drivers/video/Kconfig, etc.

Le fichier Kconfig global de l'architecture est utilisé pour présenter à l'utilisateur des choix à l'aide d'un script en mode console, ou bien graphique (le luxe !)

Ainsi :

make xconfig

lance le programme permettant de paramétrer son noyau avec une application QT.
Une fois les choix effectués, ils sont enregistrés dans un fichier .config et utilisés pour regénérer les headers.
Le fichier Kconfig a donc été adapté, pour ajouter les lignes qui sont relatives à l'Explora401 et à cette architecture. Principalement :

config MMU
bool
default n
(...)
config KERNEL_START_BOOL
bool "Set custom kernel base address"
default y
depends on ADVANCED_OPTIONS

config KERNEL_START
hex "Virtual address of kernel base" if KERNEL_START_BOOL
default "0x00000000"

config KERNEL_PHYSICAL_OFFSET
hex "Address of kernel load (decompression point)"
default "0x81000000"

config MEMORY_START
hex "Start of physical memory (also sets PAGE_OFFSET)"
default "0x81000000"
(...)
config BOOT_LOAD
        hex "Link/load address for booting" if BOOT_LOAD_BOOL
        default "0x81800000" if EXPLORA401
        default "0x00400000" if 40x || 8xx || 8260
        default "0x01000000" if 44x
        default "0x00800000"

config BLK_DEV_INITRD
        bool "Initial Ram Disk"
        depends on EXPLORA401

Il est également nécessaire de préciser à la compilation quel type d'architecture on souhaite compiler, et quel est le préfixe pour la chaine de compilation. Pour cela le fichier Makefile racine doit être adapté :

ARCH            ?= ppcnommu
CROSS_COMPILE   ?= powerpc-linux-

Création de l'architecture Explora

Chaque architecture PPC doit disposer de son entrée dans arch/ppcnommu/platforms/ afin d'y mettre ses spécificités.

Comme précisé plus-haut, le petit-frère de l'explora401, l'Explora451, dispose également d'un portage bien à lui, mais pour un noyau 2.4. Afin de réutiliser le travail réalisé pour cette machine, le fichier explora.c a été recopié vers arch/ppcnommu/platforms/4xx/explora401.c, ainsi que explora.h.


Le makefile associé au choix d'architecture doit également refléter ce choix d'architecture. Encore une fois, le noyau est prévu pour cela, puisque la définition de la variable qui reflète l'architecture est définie également dans le Makefile. Ainsi, si tous les fichiers contenus dans la variable "obj-y" sont compilés, la ligne suivante

obj-$(CONFIG_EXPLORA401)        += explora401.o

permet de demander de compiler le fichier explora.c et le tour est joué.
Ce fichier doit néanmoins être adapté pour satisfaire

Premier étage

Le démarrage du noyau est découpé en 2 étapes principales, le "gros" de celui-ci se retrouvant compressé pour prendre moins d'espace mémoire, ce qui est parfois nécessaire sur les architectures ne pouvant pas charger un gros noyau, ou quand celui-ci est téléchargé du réseau (comme ici). Il est alors nécessaire de garder non-compressée la partie du noyau qui décompressera le reste du noyau à l'endroit voulu. On appelle 1er étage la partie qui décompresse le gros du noyau, le 2nd étage. Pour résumer, un schéma :




Le but du premier étage est donc de décompresser à un point fixe le 2nd étage puis de se lancer dedans. Il initialise également une partie du matériel.

Un point important dans la machine Explora401 est que la RAM n'est pas accessible à l'adresse 0 mais à l'adresse 0x8100000. Cela a des conséquences que nous allons détailler.

Deux paramètres sont définis pour permettre de modifier ces valeurs; il s'agit de KERNEL_PHYSICAL_OFFSET qui donne le point de décompression du 2nd étage, et BOOT_LOAD qui donne le poitn de décompression du 1er étage, qui est le point ou sera chargé l'image du noyau, sur lequel on peut rarement intervenir, puisqu'elle dépend d'un chargeur plus bas-niveau.

La toute première instruction éxécutée par le noyau se trouve dans le fichier "arch/ppcnommu/boot/simple/head.S"

        .globl  start
start:
        bl      start_
        .long   0x60000000
        .long   0x60000000
        .long   0x00000020
        .ascii "XncdPPC\0"
        .long   0,0
        .long   return, 0      
return: blr

Cela permet de mettre en place des octets de signature utilisés par le firmware du NCD pour reconnaitre qu'il s'agit bien d'une image à charger et éxécuter la première instruction. En l'occurence, celle-ci est un saut à l'étiquette "_start", qui contient le vrai code. Cette adresse contient ensuite un saut à l'adresse "relocate", contenu dans le fichier "arch/ppcnommu/boot/simple/relocate.S"

Dans la version avec MMU, cette partie s'occupe de mettre en place le cache, la MMU, la TLB (Translation Look-aside Buffer, pour accélérer les calculs de page), la pile, et des arguments à passer à la prochaine fonction appelée, telle que la taille du 2nd étage. Un point intéressant est la pile qui est un bloc de 4ko de données définie ainsi :
       .comm   .stack,4096*2,4
Celle-ci est mise en place avec le code suivant :
90:     mr      r9,r1           /* Save old stack pointer (in case it matters) */
        lis     r1,.stack@h
        ori     r1,r1,.stack@l
        addi    r1,r1,4096*2
        subi    r1,r1,256
        li      r2,0x000F       /* Mask pointer to 16-byte boundary */
        andc    r1,r1,r2
On peut maintenant sauter en toute quiétude dans la 1ière fonction C àl'aide d'un appel de fonction classique:
bl      load_kernel
Celle-ci se trouve dans "arch/ppcnommu/boot/simple/misc.c"

Cette fonction a pour but principal de décompresser le 2nd étage au point donné par le paramétrage du fichier de configuration :
KERNEL_PHYSICAL_OFFSET
Pour cela, le noyau embarque le code de décompression ZIP dans son premier étage, dans le répertoire "lib/zlib_deflate/". Un appel :
gunzip( CONFIG_KERNEL_PHYSICAL_OFFSET, 0x400000 ,zimage_start, &zimage_size);
permet de décompresser le noyau à l'adresse voulue.

La mémoire utilisée est spécifiée "en dur" en prenant en compte les paramètres passés au noyau, mais aussi la détection automatique. Dans le cas du code pour l'explora, tout est encodé en dur. (et il faudrait que ca change ;) )

Afin de voir le bon déroulement du processus de boot et détecter des possibles erreurs, le premier étage embarque également le code d'initialisation du port série. Deux routines basiques, puthex et puts permettent déjà de sortir de l'affichage sur le port série.

A la fin, retour à la routine assembleur, qui se charge de sauter à l'adresse KERNEL_PHYSICAL_OFFSET.
GETSYM(r9,CONFIG_KERNEL_PHYSICAL_OFFSET)
mtlr    r9
blr
Fin du premier étage, longue vie au second étage !

A propos de la construction des étages

Le 2nd étage est placé à la racine du noyau (vmlinux), tandis que le 2nd étage est placé dans arch/ppcnommu/images/simple/.
On y trouve le noyau compressé "vmlinux.gz", le 2nd étage au format ELF "zImage.initrd.elf", et transformé pour être passé au chargeur de démarrage de l'explora (l'équivalent du BIOS pour le PC), en "zImage.initrd.explora".
La procédure de construction permet ainsi de s'affranchir jusqu'au dernier moment des spécificités de chaque architecture, ce qui permet d'avoir un code compact, factorisé au maximum pour toutes les machines, et indépendant d'un type d'assembleur donné. On retrouve cela dans arch/ppcnommu/Makefile,

Il existe cependant des routines spécifiques à l'architecture qui doivent être définies et prêtes à l'emploi à la compilation des parties du noyau indépendantes de l'architecture (et pas seulement leur signature). Celles-ci sont placées dans le répertoire d'include de l'architecture (include/asm-xxx) en question, sous forme d'inline ou bien de #define.

Second étage

Le point d'entrée est définie dans "arch/ppcnommu/kernel/head_4xx.S" et s'appelle "_start". Le fichier de lien "arch/ppcnommu/kernel/vmlinux" se charge de placer ce symbole en premier dans l'image générée.

powerpc-linux-objdump -d vmlinux 
permet de s'en assurer :
vmlinux:     format de fichier elf32-powerpc

Déassemblage de la section .text:

81000000 <_start>:
81000000:       7c 7f 1b 78     mr      r31,r3
81000004:       7c 9e 23 78     mr      r30,r4
81000008:       7c bd 2b 78     mr      r29,r5
8100000c:       7c dc 33 78     mr      r28,r6
81000010:       7c fb 3b 78     mr      r27,r7
A ce moment-là dans le code PPC 4xx, diverses routines permettent de mettre en place la MMU. Le code est içi laissé en l'état, en faisant le nettoyage de ce qui concerne la MMU, la TLB, etc. Ainsi les routines de synchronisation, flush de la tlb, etc. sont gardées mais en désactivant le code de façon à fonctionner correctement pour le 403GA. Cependant, une partie se montre intéressante, dans "initial_mmu":
    /* Establish the exception vector base
    modified to access the CONFIG_KERNEL_PHYSICAL_OFFSET, and not KERNELBASE --mathieu */
    lis    r4,CONFIG_KERNEL_PHYSICAL_OFFSET@h        /* EVPR only uses the high 16-bits */
    addis    r0,r4,0   
     mtspr    SPRN_EVPR,r0

    blr
Le registre SPRN_EVPR est rempli avec l'adress haute (16 bits de poids fort) du noyau. Dans les processeurs PPC, ce registre est utilisé lors d'une interruption ou d'une exception pour calculer l'adresse à laquelle le processeur doit sauter. Par exemple, la "Machine Check Exception", (qui se produit très fréquemment lorsqu'on traffique son noyau, sigh) se voit attribuer du numéro 0x300, ce qui signifie que le processeur doit sauter à l'adresse SPRN_EVPR+0x300 en cas d'exception de ce type. Cela se produira bien sûr uniquement pour les exceptions/interruptions autorisées par le registre d'état du processeur (voir plus bas).

Le code turn_mmu est aussi intéressant :

turn_on_mmu:
        lis     r0,MSR_KERNEL@h
        ori     r0,r0,MSR_KERNEL@l
        mtspr   SRR1,r0
        lis     r0,start_here@h
        ori     r0,r0,start_here@l
        mtspr   SRR0,r0
        SYNC
        rfi                             /* enables MMU */
        b       .                       /* prevent prefetch past rfi */

Ce type de code est présent à plusieurs endroits dans le code PPC. il permet de charger 2 registres spéciaux, SRR0 et SRR1 avec des valeurs particulières et d'effectuer un RFI (return from interrupt). Cette instruction va prendre le contenu de SRR0 et le mettre dans son registre d'état, et ensuite placer son pointeur d'instruction sur le contenu de SRR1. Ainsi, si le processeur était dans un état interrompu, en positionnant les flags de SRR0 correctement, il revient dans son flux d'éxécution "normal". Dans ce cas précis, MSR_KERNEL correspond à :

#define MSR_KERNEL      (MSR_ME|MSR_RI|MSR_IR|MSR_DR|MSR_CE|MSR_DE)

(fichier "include/asm/reg_booke.h"), avec :

#define MSR_ME          (1<<12)         /* Machine Check Enable */
#define MSR_RI          (1<<1)          /* Recoverable Exception */
#define MSR_IR          (1<<5)          /* Instruction Relocate */
#define MSR_DR          (1<<4)          /* Data Relocate */
#define MSR_CE (1<<17) /* Critical Interrupt Enable */
#define MSR_DE (1<<9) /* Debug Exception Enable */

t correspond à l'état du noyau "normal", en opposition avec un état MSR_USER, dans lequel il retourne quand il revient sur un programme :

MSR_USER        (MSR_KERNEL|MSR_PR|MSR_EE)

Avec les définitions supplémentaires :

#define MSR_PR          (1<<14)         /* Problem State / Privilege Level */
#define MSR_EE (1<<15) /* External Interrupt Enable */

Ou il peut être interrompu parce que le programme a éxécuté une instruction réservée au mode privilégié (noyau) ou une interruption matérielle est survenue.

Le noyau initialise ensuite son premier contexte d'éxécution:

        /* ptr to current */
        lis     r2,init_task@h
        ori     r2,r2,init_task@l

        /* ptr to phys current thread */
        tophys(r4,r2)
        addi    r4,r4,THREAD    /* init task's THREAD */
        mtspr   SPRG3,r4

        /* stack */

        lis     r1,init_thread_union@ha
        addi    r1,r1,init_thread_union@l

        li      r0,0
        stwu    r0,THREAD_SIZE-STACK_FRAME_OVERHEAD(r1)

bl      early_init      /* We have to do this with MMU on */

Un contexte d'éxécution est une portion de données dans laquelle le noyau stocke une pile noyau de petite taille et non-extensible, ainsi que les informations d'états, la sauvegarde de ses registres lorsqu'il sort du contexte utilisateur, etc. Le contexte courant "init_task" est un espace créé "en dur" à la compilation du noyau de façon à ne pas avoir à allouer une quelconque zone à ce niveau du boot, ce qui serait impossible, vu qu'aucun allocateur mémoire n'a été mis en place. A partir de ce moment-là, en cas d'erreur ou d'interruption (Machine Check Exception, Alignment Exception, Stack Overflow, Interrupt, Timer, etc) il est possible de tracer le processus. Nous verrons plus loin de quel manière.

On s'aperçoit içi de l'utilisation d'une première macro adaptée pour le 403GA : "tophys", pour lequel il existe un pendant "tovirt". Ces macro permettent de passer de la mémoire physique à la mémoire mappée, ce qui n'a pour nous aucun sens. Ces définitions sont donc :

#define tophys(rd,rs)                           \
        addis   rd,rs,0

#define tovirt(rd,rs)                           \
        addis   rd,rs,0

(fichier "include/asm-ppnommu/ppc-asm.h")

Le processeur saute ensuite à early_init, qui est la première routine C, qui est situé dans "arch/ppcnommu/kernel/setup.c". Ce code initialise la BSS en mettant à 0 les octets entre "__bss_start" et "_end", ce qui suppose que le plan mémoire place la BSS à la fin.

Retour à l'assembleur en renvoyant la taille de la mémoire ce qui n'a aucune importance dans le cas du 403GA. il saute alors à machine_init puis MMU_init qui sont situées également dans "arch/ppcnommu/kernel/setup.c". Machine_init a pour rôle d'appeler platform_init qui retrouve les informations du ramdisk et de la ligne de commande, puis des initialisations très bas-niveau liées à l'architecture. Il met en place "ppc_md" qui est une structure contenant des pointeurs vers des routines spécifiques à l'architecture, comme ". MMU_init récupère la taille mémoire, les mappings IO, et initialise la MMU sur l'architecture PPC normale. Dans notre cas bien sûr, la routine est grandement simplifiée !

De retour dans l'assembleur, il est temps de sauter vers start_kernel, qui est le tout premier code C non-spécifique à l'architecture. Il est situé dans "init/main.c". A partir de ce moment-là, la routine d'initialisation est celle du noyau, avec des appels aux routines spécifiques du PPC.

Routine Timer du 403GA

Une étape importante du noyau consiste à activer le timer du processeur. A ce moment-là, un décrémenteur automatique est mis en place  par le processeur.  A chaque TICK de l'horloge interne, ce compteur est diminué de 1. Lorsque le compteur atteint 0, le processeur fait une TimerInterruption et rentre dans le code d'interruption approprié. Entre-temps, le timer s'est réinitialisé à sa valeur initiale, aucun cycle n'a été perdu. De cette manière, il est possible d'égréner le temps sans aucune perte.

Pour une raison que j'ignore, le timer n'est pas réinitialisé correctement, et revient à une valeur très faible, ce qui conduit à passer tout son temps dans l'interruption timer. Pour contrer cela, le timer est réinitialisé de force, ce qui conduit à perdre des ticks. Le temps s'écoule donc plus lentement.... Et les fonctions de temps sont alors faussées !

Boot Allocator

Lorsque le processeur entre dans "start_kernel", il ne possède pas encore d'allocateur mémoire. Afin de pallier à ce problème, un allocateur mémoire temporaire est mis en place, qui permet d'allouer des blocs de manière simple mais très inefficace. Celui-ci s'appelle le "boot allocator". Cela est nécessaire par exemple sur les machines disposant de zones mémoires non-contigues.
Une fois les routines d'initalisations terminées, le noyau initialise son processus d'allocation normal et se débarasse du "boot-allocator".
Pour que ce dernier fonctionne correctement, il faut néanmoins lui passer des paramètres corrects en ce qui concerne les zones mémoires disponibles. Cela se passe dans "arch/ppcnommu/mm/init.c" dans la fonction "do_init_bootmem".
Bien que le 403GA ne dispose pas du mécanisme de pagination, l'allocation mémoire s'appuie sur un mécanisme de pages qui sont l'unité atomique réservable. La page a une taille définie par 2 ^ PAGE_SHIFT (include/asm/page.h). Sur le PPC, PAGE_SHIFT vaut 12, la page a donc une taille de 4096 octets. Pour comprendre ce qui se passe, il faut maintenant faire le distinguo entre 3 notions différentes relatives aux pages:

Grâce à ces instructions :

boot_mapsize = init_bootmem_node(&contig_page_data, min_low_pfn,
                                         CONFIG_MEMORY_START >> PAGE_SHIFT,
                                         max_low_pfn);
/* remove the bootmem bitmap from the available memory */
mem_pieces_remove(&phys_avail, start, boot_mapsize, 1);

/* add everything in phys_avail into the bootmem map */
for (i = 0; i < phys_avail.n_regions; ++i)
{
printk("Free bootmem addr %p taille %p",phys_avail.regions[i].address,phys_avail.regions[i].size);
free_bootmem(phys_avail.regions[i].address,
phys_avail.regions[i].size);
}
init_bootmem_done = 1;

On peut ainsi créer un noeud mémoire et passer toute la mémoire de ce noeud à l'état disponible, en utilisant la notion de pfn.

Allocation Mémoire standard

L'allocateur mémoire va ensuite prendre le relai dans l'éxécution du noyau. Il a besoin d'informations supplémentaires permettant de faire le lien entre les pfn, les pages, les indices de page, etc.

Ces informations se trouvent dans include/asm/page.h

Dans une architecture classique avec MMU, un ensemble de fonctions permettent de passer des adresses virtuelles aux adresses réelles et vice-versa. Seul le noyau a connaissance de la correspondance car cela est transparent pour les applications en espace utilisateur. Pour les adresses du noyau, il s'agit d'une correspondance linéaire ou un offset est rajouté ou retranché pour passer d'un type d'adresse à l'autre. Cela se traduit par des macros assez simples. Dans notre cas, c'est encore plus simple, la corrélation est directe car il n'y a pas de mécanisme de pagination.
#define ___pa(vaddr) (vaddr)
#define ___va(paddr) (paddr)

#define __pa(x) ___pa((unsigned long)(x))
#define __va(x) ((void *)(___va((unsigned long)(x))))

PAGE_OFFSET est défini à l'adresse de début de la mémoire. Obtenir l'indice dans la table des pages devient ici un jeu d'enfant:

#define MAP_NR(addr)            (((unsigned long)(addr)-PAGE_OFFSET) >> PAGE_SHIFT)

Le PFN (page frame number) est quant à lui indicé par rapport au début de la mémoire. Les conversions sont un jeu arithmétique:

#define virt_to_pfn(kaddr)      (__pa(kaddr) >> PAGE_SHIFT)
#define pfn_to_virt(pfn)        __va((pfn) << PAGE_SHIFT)

#define virt_to_page(addr)      (mem_map + (((unsigned long)(addr)-PAGE_OFFSET) >> PAGE_SHIFT))
#define page_to_virt(page)      ((((page) - mem_map) << PAGE_SHIFT) + PAGE_OFFSET)

#define pfn_to_page(pfn)        virt_to_page(pfn_to_virt(pfn))
#define page_to_pfn(page)       virt_to_pfn(page_to_virt(page))

Savoir si une page est valide revient à tester si elle ne dépasse pas le nombre maximum de pages, qui est défini par l'architecture en fonction de la mémoire totale.

#define VALID_PAGE(page)        ((page - mem_map) < max_mapnr)

Vérifier si une adresse virtuelle est valide revient à revient si elle est dans les bornes de la mémoire physique

#define virt_addr_valid(kaddr)  (((void *)(kaddr) >= (void *)PAGE_OFFSET) && \
                                ((void *)(kaddr) < (void *)memory_end))

Cette macro est requise, mais on fait confiance au noyau pour avoir effectuer d'autres tests avant :

#define pfn_valid(pfn)  (1)

Format BINFMT_FLAT pour PowerPC

Il existe différents formats d'éxécutables pris en charge par le noyau. Le code associé se trouve dans fs/. On y trouve ainsi:

Les formats elf et aout ont l'inconvénient d'être verbeux et peut adaptés aux cibles embarqués. Le format "flat" a l'avantage d'être compact, et surtout de contenir toutes les informations pour effectuer une relocation.

En effet, les format ELF et consors sont basés sur le principe que la cible contient une MMU, et ainsi que chaque programme a son espace mémoire virtuel. Il est alors possible de fixer à l'avance à quel endroit doit se trouver le code à éxécuter (section TEXT), les données statiques pré-initialisées (DATA) et la section de données non-initialisées (BSS, en fait initialisée de force à 0 par le noyau).

NOTE : Dans le cas des bibliothèques partagées, c'est un peu plus compliqué, et certains symboles doivent être trouvés à des endroits précis. Passons.


Or le PPC403GA ne contient pas de MMU. Chaque programme se retrouvera donc à une adresse X, impossible à déterminer à l'avance. Si le programme est bati uniquement sur du code PIC (Position Independant Code), c'est parfait, toutes les adresses dont le programme à besoin (cibles des sauts, position des données) sont encodées par une adresse relative, et quelque soit la position du programme, il retrouvera parfaitement ses petits.

Ce n'est pas le cas du code généré par le 403GA, les adresses sont encodées "en dur", de manière absolue.

Il faut donc trouver un moyen pour "relocaliser" (relocation) les références à des adresses absolue vers leur adresse absolue finale.

Pour cela, une liste des relocations à effectuer est fournie par le fichier binaire du programme à éxécuter, dont voici une vue schématique :



Dans binfmt_flat.c, chaque adresse est passée en revue et chacune est traitée:

 1                for (i=0; i < relocs; i++) {
 2                        unsigned long addr, relval;
 3                        /* Get the address of the pointer to be
 4                           relocated (of course, the address has to be
 5                           relocated first).  */
 6                        relval = ntohl(reloc[i]);
 7                        addr = flat_get_relocate_addr(relval);
 8                        rp = (unsigned long *) calc_reloc(addr, libinfo, id, 1);
 9                        if (rp == (unsigned long *)RELOC_FAILED)
 10                               return -ENOEXEC;
 11                       /* Get the pointer's value.  */
 12                       addr = flat_get_addr_from_rp(rp, relval, flags);
 13                       if (addr != 0) {
 14                               /*
 15                                * Do the relocation.  PIC relocs in the data section are
 16                                * already in target order
 17                                */
 18                               if ((flags & FLAT_FLAG_GOTPIC) == 0)
 19                                       addr = ntohl(addr);
 20                               addr = calc_reloc(addr, libinfo, id, 0);
 21                               if (addr == RELOC_FAILED)
 22                                       return -ENOEXEC;
 23                               /* Write back the relocated pointer.  */
 24                               flat_put_addr_at_rp(rp, addr, relval);
 25                       }
  1. "flat_get_relocate_addr" (ligne 7, fonction spécifique à l'architecture) construit l'adresse qui devra contenir l'adresse relocalisée. il y a nécessité de passer par une fonction spécifique à l'architecture, car celle-ci intègre des drapeaux qui n'ont rien à voir avec l'adresse cible.

  2. Cette adresse qui contiendra l'adresse à relocaliser doit elle-même être transformée en fonction de l'adresse de chargement du programme. Cette adresse est décalée par la fonction calc_reloc (ligne 8)

  3. "flat_get_addr_from_rp" (ligne 12, fonction spécifique à l'architecture) récupère à l'adresse spécifiée la valeur de l'adresse cible relative. Il y a nécessité de passer par une fonction spécifique à l'architecture, car l'adresse cible peut être sur 16, 24, 32 bits, contenir des drapeaux, etc.

  4. Cette adresse est décalée en fonction de sa position par la fonction calc_reloc (ligne 20)

La fonction "calc_reloc" n'est pas spécifique à l'architecture, et calcule elle si l'adresse passée est dans la section TEXT ou DATA. Sur certains processeurs sans MMU, seul la section DATA doit être relocalisée, car la seciton TEXT peut être partagée par plusieurs processus. C'est le cas des processeurs générant du code PIC, avec un registre contenant l'origine de la section DATA.

Entrons dans les détails de notre cible ppcnommu. Dans arch/ppcnommu/process.c, on trouve les définitions des fonctions précédemment évoquées. La table de relocation contient des entrées sur 32 bits, dont 3 bits de drapeaux. Cela laisse 29 bits d'adresse, soit 512Mo, ce qui est largement suffisant pour adresser un programme complet. Ces 3 bits de drapeaux sont positionnés en tête de l'entrée et ont la signification suivante :

L'adresse cible à transformer et stockée est stockée temporairement à l'adresse qui sera justement relocalisée. Or dans le cas des relocations sur 16 bits, il est nécessaire d'avoir l'adresse complète sur 32 bits. En effet,
HA(X)= ((( X >> 16 ) + (( X & 0x8000 ) ? 1 : 0 )) & 0xFFFF ) 

On a donc besoin des 16 bits de poids faibles (à cause de "X & 0x8000", bien sûr !)

Comme la relocation temporaire dans le code ne contient que 16 bits, il faut trouver un endroit pour stocker les autres 16 bits. Ils sont donc stockés dans une relocation donc l'unique but est de fournir temporairement 16 bits d'information pour la relocation suivante. (0xC0000000)

NOTE : Ce mécanisme est nécessaire uniquement pour les programmes utilisateurs et pas pour le noyau. En effet, ce dernier est chargé à une adresse prédéterminée, qu'il s'agisse du 1er ou du 2nd étage, et de ce fait, les adresses sont connues à l'avance.

Appels systèmes vfork

Il existe 2 manières d'effectuer un "fork" sur un système linux : un appel système à "fork" ou un autre à "vfork". "Fork" utilise le mécanisme COW (Copy-On-Write), ce qui signifie que les pages de mémoire du père ne sont pas recopiées tant que le fils n'a pas écrit dessus. L'intérêt est que père et fils partagent les mêmes pages, et peuvent continuer à éxécuter le même programme.
Ceci n'est pas possible sur un système sans MMU, puisqu'il n'y pas de mécanisme de pagination. Inévitablement à un moment donné, père et fils vont se marcher sur les pieds.
En revanche, vfork bloque le père jusqu'à ce que le fils effectue un appel à "execve" ou un appel à "exit". "execve" aura pour conséquence d'allouer au fils son propre espace TEXT/DATA/BSS, et père et fils vivront leur vie séparément. Dans l'hypothèse ou le fils se met à faire quelque chose entre son fork et un appel execve (ou exit), c'est la catastophe assurée. Le cas de l'appel exit pour le fils est plus trivial : le fils se suicide, et le père reprend la main.
Un système uClinux est ainsi limité dans son usage, et seuls les programmes prenant en compte cette limitation pourront fonctionner correctement.

Afin d'adapter le PowerPC à cette spécificité, le fichier "arch/ppcnommu/kernel/process.c" est modifié et devient :
int sys_fork(int p1, int p2, int p3, int p4, int p5, int p6,
         struct pt_regs *regs)
{
    return (-EINVAL);
}

int sys_vfork(int p1, int p2, int p3, int p4, int p5, int p6,
          struct pt_regs *regs)
{
    CHECK_FULL_REGS(regs);
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->gpr[1],
            regs, 0, NULL, NULL);
}

Inutile d'autoriser les programmes à éxécuter un appel système qui leur permettra de courir à la catastrophe...

Système de fichier embarqué

Afin de pouvoir utiliser ce noyau, il est nécessaire de pouvoir lancer des programmes, et pour cela, d'avoir un système de fichier racine. Comme le NCD ne dispose pas de disque dur, ni de périphérique de stockage de masse (à part le PCMCIA, mais il faut y rajouter une carte), il est nécessaire :

C'est cette 2nde option, plus flexible, qui a été retenue.


Pour ce système de fichier, il faut choisir un type de système, parmi tous ceux supportés par Linux, de préférence un système de fichier peu gourmand en mémoire, ou l'écriture sera aisée, et ou toutes les fonctionnalités du noyau serait disponible; La facilité de réalisation aidant, le choix s'est porté sur le système "ext2", surement le plus connu sous Linux.


Pour cela, il est nécessaire :

Réaliser une image hôte

Sous linux, il est possible de créer un système de fichier dans un fichier et de le monter en loopback pour y accéder. Les commandes suivantes :

cd PATH/TO/IMAGE
dd if=/dev/zero of=initrd.img bs=1k count=2048
/sbin/mkfs.ext2 -i 1024 -b 1024 -m 5 -F -v initrd.img
mount initrd.img TMP -t ext2 -o loop

permettent ainsi de créer et monter le fichier "initrd.img" dans le répertoire "TMP" comme étant un système de fichier ext2.

On peut alors  le remplir avec le ce qui est nécessaire et une fois terminé, démonter le périphérique :

umount TMP

Tous les fichiers du système de fichier sont alors dans l'image "initrd.img"

Poser l'image dans le noyau

Pour cela, rien de plus simple,

cp PATH/TO/initrd.img arch/ppcnommu/boot/images/ramdisk.image.gz

Une fois la recompilation du noyau effectuée, l'image est incluse dans le noyau comme ramdisk, et sera décompressée en temps voulue.

Reconnaitre l'image et la faire monter

Il faut passer les paramètres sur la ligne de commande du boot:

ramdisk_blocksize=1024 root=/dev/ram0 

L'image disque est alors reconnue et montée automatiquement, à condition d'avoir compilé en statique (pas en module) les drivers suivants (extrait du fichier .config du kernel):

CONFIG_EXT2_FS=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_SIZE=4096

Dans le cas ou ce serait compilé en module, non-seulement le noyau ne serait pas capable d'insérer un module à cause des relocations, mais en plus pour charger le fichier il lui faudrait le module ext2, qui est justement le module que l'on cherche à charger !

On vient ainsi de se débarasser de l'infâme message "Unable to mount root device", mais voilà qu'un autre surgit: "Kernel Panic ! Unable to run init". C'est que notre image est vide, et donc le noyau ne trouve pas de programme "init" à lancer. Voyons comment en construire un.

Elf2flt

Notre chaine de compilation est capable de générer du code statique, mais le format cible reste du "elf" (executable and linkable format). Or comme vu précédemment, nous voulons obtenir du FLAT. Le programme permettant de faire cela s'appelle elf2flt, et il est lié fortement au patch uClinux.

Cependant, il ne gère pas le PowerPC. Des adaptations sont donc nécessaires, et reflètent ce que le noyau demande.

Elf2flt contient:

  1. Un fichier shell "ld-elf2flt" qui se substitue au "ld" de la chaine de compilation.

  2. Un script "ld" qui correspond au plan mémoire que le programme elf2flt attend pour générer du flat final

  3. Un programme elf2flt.c, qui est lié à la bilibothèque "bfd" de la cible, et qui transforme les relocations de façon à donner au noyau ce qu'il attend.

  4. Un autre programme, "flthdr", qui n'est pas absolument nécessaire au bon déroulement de la compilation, mais permet d'obtenir des informations sur les binaires générés, notamment les tailles des différentes sections, les drapeaux (flags) mis en place dans les binaires, etc.

Voici un résumé graphique de ce que fait le elf2flt :

ld-elf2flt

Ce programme se substitue à ld et permet de lancer elf2flt sur le fichier elf généré. Il se sert également d'un fichier ".gdb" qui contient une partie des relocations résolues pour déterminer si le programme contient des bibliothèques partagées (en vérifiant la présence d'une GOT, Global Offset Table, utilisée dans les bibliothèques partagées). Actuellement, elles ne sont pas  gérées par notre cible PowerPC.

Le script ld prend en compte tous les fichiers objets, qu'ils proviennent du programme proprement dit ou d'une archive (.a) contenant des routines supplémentaires, comme celles de la libc. Pour finir, il est nécessaire d'y ajouter un point d'entrée, nommé "_entry" et qui est contenu dans CRT0.S compilé vers CRT0.o, qui met en place les registres de pile et autres, et appelle à son tour le point d'entrée de la libc, qui appelera ensuite le "main" contenu dans le programme. La route est longue vers ce qui semble le point d'entrée "main" du programme !

Pour des raisons de tests, on peut se passer de CRT0.o à condition de définir soit-même son point d'entrée nommé _start.


Note: les bibliothèques partagées (.so) diffèrent des bibliothèques statiques (.a) en cela que LD extrait tout le code nécessaire des bibliothèques statiques et l'inclus directement dans le fichier objet de sortie alors que les bibilothèques partagées contiennent uniquement une référence au nom de la bibilothèque, qui sera partagée par tous les programmes le nécessitant à l'éxécution.


elf2flt.ld

Le script fourni permet de grouper les sections TEXT, DATA, BSS de manière facilement identifiables à ce qu'attend le programme elf2flt.

Exemple avec la section BSS:

       .bss : {
                . = ALIGN(0x4) ;
                _sbss = ALIGN(0x4) ;
                __bss_start = . ;
                *(.dynsbss)
                *(.sbss)
                *(.sbss.*)
                *(.scommon)
                *(.dynbss)
                *(.bss)
                *(.bss.*)
                *(.bss*)
                *(.gnu.linkonce.b*)
                *(COMMON)
                . = ALIGN(0x10) ;
                _ebss = . ;
                _end = . ;
                end = . ;
        }

Une seule ligne a été rajoutée, car elle correspondait à une section manquante dans le script, mais qui est présente dans les binaires PowerPC.

*(.got2)

Comme actuellement la GOT n'est pas traitée (pas de bibliothèque partagée), ca ne change rien au final.

elf2flt.c

C'est ici qu'est la partie la plus importante du package elf2flt. Ce programme simple effectue les opérations suivantes:

Pour chacune des sections BSS, TEXT, DATA, il recherche les relocations qui sont nécessaires. Ces relocations indique au chargeur de programme qu'une adresse doit être recalculée, et que cette adresse ne peut pas être connue statiquement. En effet, le compilateur C génère des adresses absolue vers des sections autres que celle ou la relocation se trouve, et il n'est pas possible de déterminer l'adresse de

Le but du format FLAT est de mettre bout à bout TEXT/DATA/BSS afin de pouvoir calculer facilement ces relocations. Une table de relocation contient la liste des adresses à l'intérieur du fichier éxécutable qu'il faut relocaliser.


Tout cela a été vu dans la section noyau, penchons-nous sur la génération de cette table de relocations. Comme nous ne considérons pas le cas des bibliothèques partagées, les relocations prises en compte sont les suivantes :

La marche à suivre est assez simple : pour chaque relocation de chacune des sections TEXT DATA et BSS, on applique un switch selon le type de relocation. A l'intérieur de celle-ci, la bibliothèque libbfd (binary format descriptor) compilée pour notre PowerPC et liée statiquement au programme elf2flt (pour être sûr de ne pas tomber sur celle de la machine hôte !) nous aide à déterminer l'adresse de la cible. En fonction de celle-ci on peut réaliser les

R_PPC_REL32 et R_PPC_REL24

Les adresses relatives sur 24 bits sont les plus plus faciles à traiter. En effet, elles constituent une exception car elles ne correspondent pas à une adresse absolue dans le fichier, mais relative, dans la section courante TEXT vers la section TEXT. Cela signifie qu'on peut directement mettre la bonne valeur à l'adresse donnée pour la relocation. Une adresse REL32 correspondant à une adresse finale sur 32 bits, une adresse REL24 correspond à une adresse sur.... 24 bits !

L'adresse calculée est la différence entre l'adresse cible et l'adresse contenant la relocation.

Mais attention, c'est une adresse relative, et on peut parfois avoir besoin d'1 bit de différence; dans ce cas, la valeur qui est stockée dans l'adresse finale doit être ajoutée à l'adresse calculée.

Pour ces relocations, il n'est donc pas nécessaire d'ajouter une entrée de relocation :

relocation_needed = 0;

Pour les adresses relatives sur 32 bits, elles peuvent quand à elles avoir pour cible une autre section que la section en cours. il est donc nécessaire de créer une entrée spécifique pour que le noyau s'en occupe. Ce qui donne  :

case R_PPC_REL32:
                                  {
                                  relocation_needed = 1;
                                  pflags=0x20000000;
                                  sym_vma = bfd_section_vma(abs_bfd, sym_section);
                                  sym_addr += sym_vma + q->addend;
                                  break;
                                  }

R_PPC_ADDR16_LO et R_PPC_ADDR16_HA

Ces deux-là fonctionnent ensemble de manière symétrique; L'adresse finale est une adresse sur 16 bits, ce qui signifie que seuls 16 bits de la cible devront être écrits. Les 16 bits suivants ne doivent pas être touchés. Le problème est que la cible doit stocker une adresse cible complète sur 32 bits, alors que l'espace laissé est de 16 bits. Comme on l'a vu précédemment dans la paragraphe consacré au noyau, la solution consiste donc à rajouter une entrée de relocation bidon d'un type bidon contenant les 16 autres bits, juste avant notre entrée de relocation. Cette relocation ne se sera pas utilisée en tant que telle, mais permettra de calculer la valeur complète.

R_PPC_ADDR32

C'est une adresse absolue sur 32 bits; on stocke donc directement l'adresse relative à l'adresse cible. Le noyau fera le reste.

uClibc

Compiler uClibc directement nécessite d'appliquer des patchs pour le faire cohabiter avec le compilateur, et ensuite avec busybox, etc. Un outil existe déjà et a été développé spécifiquement pour cela, nommé BuildRoot; il permet de construire de A à Z un système embarqué, en téléchargeant les bonnes versions de la chaine de compilation, de uClibc, elf2flt, busybox, construit le système de fichiers à embarquer, le noyau, etc.

Malheureusement, il ne convient pas à notre besoin. (C'est la loi de Murphy : tout ce qui a une chance de rater ratera au moment crucial). En effet, il nous faut une chaine PowerPC sans MMU, qui n'a pas encore été développée. A l'aide de BuildRoot, on peut néanmoins lancer la compilation des  uClibc, mais il faut inévitablement mettre "les mains dans le cambouis" pour aller adapter le logiciel. Le choix de la configuration "powerpc" permet d'obtenir une chaine de compilation et uClibc, avec les patchs nécessaires. (modulo tous les soucis d'installation du aux différences entre distributions, qui font la joie du développement sous Unix)

Il faut maintenant adapter la compilation pour se passer de la MMU.

Ces outils reprennent le mode de configuration développé pour le noyau, en s'appuyant sur des fichiers de configuration ".config" générés par "make config". En éditant le fichier ".config", on peut donc ajouter les spécificités du PowerPC.

TARGET_powerpc=y
TARGET_ARCH="powerpc"
ARCH_SUPPORTS_BIG_ENDIAN=y
ARCH_BIG_ENDIAN=y
# ARCH_HAS_NO_MMU is not set
# ARCH_HAS_MMU is not set
# UCLIBC_HAS_FLOATS is not set
# HAVE_NO_PIC is not set
# DOPIC is not set
# HAVE_NO_SHARED is not set
# ARCH_HAS_NO_LDSO is not set
# UCLIBC_CTOR_DTOR is not set
# HAS_NO_THREADS is not set
# UCLIBC_HAS_THREADS is not set
# UCLIBC_HAS_LFS is not set
MALLOC=y

Mais ce n'est pas tout. En effet, en fonctionnant tel quel, uClibc n'est pas capable de faire un vfork correct. En effet, dans cette version (0.9.27), uClibc ne fait pas de vfork avec le PowerPC:

pid_t vfork(void)
{
        unsigned long __sc_ret, __sc_err;
        register unsigned long __sc_0 __asm__ ("r0");
        register unsigned long __sc_3 __asm__ ("r3");

#if 0
        /* Sigh.  The vfork system call on powerpc
         * seems to be completely broken.  So just
         * use fork instead */

        __sc_0 = __NR_vfork;
        __asm__ __volatile__
                ("sc            \n\t"
                 "mfcr %1       "
                : "=&r" (__sc_3), "=&r" (__sc_0)
                : "0"   (__sc_3), "1"   (__sc_0)
                : __syscall_clobbers);
        __sc_ret = __sc_3;
        __sc_err = __sc_0;

        if((__sc_err & 0x10000000) && (__sc_ret == ENOSYS))

#endif
        {
                __sc_0 = __NR_fork;
                __asm__ __volatile__
                        ("sc            \n\t"
                         "mfcr %1       "
                        : "=&r" (__sc_3), "=&r" (__sc_0)
                        : "0"   (__sc_3), "1"   (__sc_0)
                        : __syscall_clobbers);
                __sc_ret = __sc_3;
                __sc_err = __sc_0;
        }
        __syscall_return (pid_t);
}

Or avec nos modifications, le vfork passe maintenant sans problème, ce qui autorise, à passer de

__sc_0 = __NR_fork;
à
__sc_0 = __NR_vfork;

Et le tour est joué.

BusyBox

BusyBOx est un outil très utile permettant à l'aide d'un seul binaire, de construire un système Linux presque complet. En effet, selon la manière dont il est invoqué (le premier argument est le nom du programme binaire), il va se comporter comme si cette commande avait été appelée sous Unix. Par exemple, si le binaire s'appele "cp", busybox agira comme la commande "cp". Il suffit donc d'1 seul busybox dans tout le système de fichiers et d'un lien symbolique pour chacune des commandes qu'il supporte pour obtenir tout ce dont un système complet a besoin, en paratant des commandes de base, "cp", "mv", "mkdir", pour aller jusqu'à "init", "syslogd" et comble du luxe, un shell interactif nommé "msh", fonctionnant parfaitement sur un système sans MMU.

Compilation

On laisse dérouler la compilation normalement jusqu'à l'étape finale de l'édition de lien. Si la chaine a été créée proprement, on doit pouvoir obtenir le binaire final sans souci; mais pour la beauté de la chose ;) on peut décomposer l'édition de lien :


powerpc-linux-gcc -O2 -static -elf2flt  -nodefaultlibs -nostdlib  -iwithprefix -Wall -Wa,-m403 -I. -Iinclude ./applets/busybox.c \
-o busybox \
-Wl,--start-group applets/applets.a archival/archival.a archival/libunarchive/libunarchive.a coreutils/coreutils.a console-tools/console-tools.a \
debianutils/debianutils.a editors/editors.a findutils/findutils.a init/init.a miscutils/miscutils.a \
modutils/modutils.a networking/networking.a networking/libiproute/libiproute.a networking/udhcp/udhcp.a \
procps/procps.a loginutils/loginutils.a shell/shell.a sysklogd/sysklogd.a util-linux/util-linux.a libpwdgrp/libpwdgrp.a \
coreutils/libcoreutils/libcoreutils.a libbb/libbb.a -Wl,--end-group -L. -L${ROOTUCLIBC}/uClibc-0.9.27/lib/ \
${CRT} -lc \
$ROOTGCC/gcc/libgcc/_divdi3.o \
/gcc/libgcc/_ashrdi3.o \
gcc/libgcc/_ashldi3.o \
gcc/libgcc/_moddi3.o \
gcc/libgcc/_udivdi3.o

Installation

Une fois BusyBox compilé, il ne reste plus qu'à créer tous les liens symboliques vers le binaire proprement dit, ce qui donne quelque chose comme :
cd bin
cp $BUSYBOX msh
cd bin
ln -s msh cat
ln -s msh chgrp
(...)
cd ../sbin
ln -s ../bin/msh init
ln -s ../bin/msh halt
ln -s ../bin/msh poweroff
ln -s ../bin/msh reboot
(...)

Attention,  les liens symboliques doivent être relatifs au système de fichier image, le système embarqué prenant comme racine une racine différente de celle du système hôte.

Démarrer avec un shell

Et voici l'étape finale, le "bouquet final", l'achèvement de ce travail. Il reste à demander au noyau à la fin de son processus de boot de démarrer un beau shell sur le port série, sur lequel il sera possible de taper les commandes Unix standard. Pour cela, éditons la ligne de commande à passer au boot :
CONFIG_CMDLINE="rw console=ttyS0,9600 ramdisk_blocksize=1024 root=/dev/ram0 init=/bin/msh"

Le noyau devrait ainsi chercher à lancer le programme "/bin/msh" au boot. Ce qui devrait nous amener à :

RAMDISK driver initialized: 16 RAM disks of 4096K size 1024 blocksize
mice: PS/2 mouse device common for all mice
RAMDISK: ext2 filesystem found at block 0
RAMDISK: Loading 2048KiB [1 disk] into ram disk... done.
VFS: Mounted root (ext2 filesystem).
Freeing unused kernel memory: 56k init
Using fallback suid method

BusyBox v1.00 (2005.09.24-12:34+0000) Built-in shell (msh)
Enter 'help' for a list of built-in commands.

#

C'en est fini !

A faire

Fini ? Non, pas tout-à-fait.
Ce système constitue maintenant une base solide pour porter des applications, telle qu'un serveur X sur cette machine. Mais avant cela, il reste à réussir à faire fonctionner les périphériques PS/2 : clavier et souris, mais également la carte réseau.

Dans une prochaine partie, le portage de Nano-X pour l'affichage graphique.

Liste des fichiers source et binaires

Les sources du noyau linux pour explora

elf2flt pour PowerPC

uClibc pour Explora (surtout la configuration et l'application des patchs)

Le noyau à charger (binaire)

Liste des documents utiles

PowerPC 403GA User's Guide


PowerPC 403GA 32 bits Risc Embedded Controller


System V Application Binary Interface PowerPC Processor Supplement


Other docs about what's inside the NCD Explora 401:

Le projet Linux sur Explora 451 : http://explora-linux.sourceforge.net/EXPLORA.html

Procédure rapide pour se connecter au NCD par le port série

Côté NCD

C'est plutôt simple pour la connexion, mais cela nécessite un cable série mâle/mâle(quelques ¤ en supermarché) comme celui-ci :


Côté NCD, rien de plus simple :


Il faut encore configurer le NCD pour aller chercher l'image à booter côté PC. Ca se passe dans le menu de paramètrage du NCD; pour cela, appuyer sur Esc au moment de l'allumage du NCD. Taper "se" pour entrer dans le setup. ("?" donne une liste des commandes, utile aussi)

  1. Aller dans "Network" à l'aide des flèches,

  2. Dans "boot",

  3. Dans "Done":


Côté machine hôte

Là, c'est légèrement plu compliqué. il faut non-seulement s'occuper de voir "logiquement" la sortie série, mais également paramétrer un serveur tftp qui contient l'image à booter. On se sert également de Linux de ce côté, pour des raisons de facilité.


Pour la partie à booter, utilisez le serveur tftp de votre distribution (tftfp-server sur Mandriva), et positionnez la racine du répertoire à "/tftpboot/" . Copiez votre noyau dans ce répertoire (Xncdxpl ou autre), et n'oubliez pas d'activer le serveur...


Il faut utiliser un programme permettant de se connecter sur le port série. "minicom" convient parfaitement. Selon le port série utilisé sur la machine hôte, utliser comme périphérique "/dev/ttyS0", ttyS1, etc.

Utiliser en 9600 bauds, et supprimer les chaines de connexion du modem, vu que ce n'est pas un modem ;)


Capture d'écran

Puisque les captures d'écran sont à la mode, en voici une :



Conclusion

Ce document devrait permettre de comprendre un peu mieux les rouages de uClinux et de son portage vers l'Explora401. Pour des questions, corrections, proposition de patch, envoyez un mail à mathieu.avila@laposte.net