Embedded GNU/Linux partendo da zero: integrazione e test

Embedded GNU/Linux from scratch

L’obiettivo di questa serie di articoli è di fornire una guida passo passo per la creazione da zero di un sistema GNU/Linux perfettamente funzionante, applicabile ad esempio in scenari di tipo embedded come base per la generazione di un semplice BSP (Board Support Package), di un firmware Linux-based, o di una micro-distribuzione totalmente custom.

Introduzione

Nelle puntate precedenti (http://it.emcelettronica.com/embedded-gnulinux-partendo-da-zero-ricompilare-kernel e http://it.emcelettronica.com/embedded-gnulinux-partendo-da-zero-preparazione-del-root-filesystem) abbiamo analizzato rispettivamente la ricompilazione del kernel e la preparazione di un root filesystem minimale usando l’initramfs.

In questa puntata vedremo come completare il root filesystem dotandolo dei tipici comandi presenti in qualsiasi distribuzione GNU/Linux. Infine vedremo alcuni esempi pratici per testare la funzionalità della micro-distribuzione custom da noi creata.

I comandi della shell

La volta scorsa ci siamo lasciati con un root filesystem dotato di un unico binario eseguibile: /init. Questo è sufficiente per permettere al kernel di lanciare una nostra applicazione custom al termine del boot, ma è abbastanza diverso da quello che si aspetta di trovare in un tipico sistema GNU/Linux.

Quello che manca è una vera e propria shell completa, dotata dei comandi che caratterizzano qualsiasi distribuzione GNU/Linux.

BusyBox

BusyBox[1] è conosciuto anche come il “coltellino svizzero” dell’embedded Linux. Si tratta di un progetto open-source (GPLv2) in grado di fornire un intero set di comandi e utility presenti nella suite GNU.

L’intero progetto è stato scritto con un particolare occhio di riguardo all’ottimizzazione dello spazio occupato dai binari, il che lo rendono particolarmente appetibile nell’ambito embedded. Difatti praticamente tutte le note piattaforme embedded Linux-based utilizzano BusyBox (evaluation board, ricevitori, dispositivi mobile come tablet, smartphone, etc. anche Android!).

Vediamo di seguito i passi necessari per integrare BusyBox nel nostro sistema.

Per prima cosa possiamo reperire i sorgenti direttamente dal repository git di BusyBox:

$ git clone git://busybox.net/busybox.git

Successivamente dobbiamo specificare la configurazione di ciò che vogliamo compilare e includere nel nostro ambiente. Analogamente a quanto abbiamo fatto con il kernel possiamo lanciare un “make menuconfig” nel modo seguente:

$ cd busybox
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
OPTIMIZE_FOR_CPU=armv7-a CONFIG_PREFIX=/tmp/initrd menuconfig

Al solito, con l’opzione CROSS_COMPILE abbiamo dato indicazione al sistema id build di BusyBox quale cross-compilatore utilizzare (lo stesso con cui abbiamo compilato precedentemente il kernel) e con ARCH=arm e OPTIMIZE_FOR_CPU abbiamo specificato l’architettura target per i file binari che dovranno essere generati. Con CONFIG_PREFIX, invece, specifichiamo la directory dove vogliamo che BusyBox vada ad installare tutti i binari.

Se non vogliamo scorrere tutti i singoli menù del “make menuconfig” di BusyBox possiamo utilizzare la configurazione riportata come allegato a questo articolo (busybox-config.txt). Per fare ciò basta lanciare i comandi seguenti:

$ wget http://it.emcelettronica.com/files/busybox-config_0.txt
$ cp busybox-config.txt busybox/.config

Una volta messa a punto la configurazione si può compilare la suite di strumenti:

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
OPTIMIZE_FOR_CPU=armv7-a CONFIG_PREFIX=/tmp/initrd install

E infine installarla nella directory di destinazione:

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
OPTIMIZE_FOR_CPU=armv7-a CONFIG_PREFIX=/tmp/initrd install

Dopo l’installazione la directory di destinazione assume la tipica struttura che troviamo nella radice di un qualsiasi sistema UNIX-based:

# ls -l /tmp/initrd
total 12
drwxrwxr-x 2 righiandr righiandr 4096 Dec 14 15:07 bin
drwxrwxr-x 2 righiandr righiandr 4096 Dec 14 15:07 sbin
drwxrwxr-x 4 righiandr righiandr 4096 Dec 14 15:01 usr

In pratica con l’installazione di BusyBox abbiamo generato la struttura base del nostro tipico filesystem UNIX-based.

All’interno della sotto-directory “bin” l’unico binario “busybox”; tutti gli altri comandi sono disponibili nel filesystem come link simbolici all’eseguibile “busybox”:

# ls -l /tmp/initrd/bin/
total 1284
lrwxrwxrwx 1 righiandr righiandr 7 Dec 14 15:07 ash -> busybox
lrwxrwxrwx 1 righiandr righiandr 7 Dec 14 15:07 base64 -> busybox
-rwxr-xr-x 1 righiandr righiandr 1314624 Dec 14 15:07 busybox
lrwxrwxrwx 1 righiandr righiandr 7 Dec 14 15:07 cat -> busybox
lrwxrwxrwx 1 righiandr righiandr 7 Dec 14 15:07 catv -> busybox
lrwxrwxrwx 1 righiandr righiandr 7 Dec 14 15:07 chattr -> busybox
lrwxrwxrwx 1 righiandr righiandr 7 Dec 14 15:07 chgrp -> busybox

Quiz #1: come mai BusyBox genera un solo binario e sopratutto a cosa servono i link simbolici?

Risposta #1: Come abbiamo visto nella puntata precedente, nel filesystem che generiamo completamente da zero non abbiamo a disposizione le librerie dinamiche (glibc, etc.); le alternative quindi sono di cross-compilare e installare anche tutte le librerie dinamiche richieste, oppure di compilare staticamente i binari (cioè includere direttamente nel binario il codice delle librerie richieste).

Nel nostro caso abbiamo compilato BusyBox staticamente, ma se per ciascun binario includessimo al suo interno il codice delle librerie richieste, avremo una duplicazione e ridondanza del codice non necessaria.

BusyBox risolve questo problema implementando il codice di ciascun comando all’interno dello stesso binario (invece di avere una separazione a livello di file, possiamo vedere un comando della shell di BusyBox come una funzione all’interno dello stesso binario).

I link simbolici costituiscono una soluzione intelligente per distinguere un comando dall’altro. Ad esempio quando eseguiamo il binario /bin/cat dalla shell, questo di fatto esegue /bin/busybox, ma durante all’interno del codice il nome del binario risulta /bin/cat.

Alla fine possiamo immaginare il “main” di BusyBox nel modo seguente (con le dovute semplificazioni del caso):

int main(int argc, char **argv)
{
if (!strcmp(argv[0], “/bin/cat”)) {
/* implementazione di cat */
} else if (!strcmp(argv[0], “/bin/ls”)) {
/* implementazione di ls */
} else …

A questo punto ci mancano solo pochi passi per completare un tipico root filesystem GNU/Linux: inizializzare /proc, /sys e /dev.

Il proc filesystem (o procfs) [3], montato genericamente sotto /proc, è uno pseudo-filesystem usato dal kernel per esportare le informazioni sui processi alle applicazioni che girano in spazio utente. Tale filesystem si trova solitamente montato nella directory /proc. Questo filesystem non utilizza nessun dispositivo di storage, è solo un’astrazione messa a disposizione del kernel per dare una visione dei processi come file, nel pieno rispetto della filosofia dell'”everything is a file” [2].

Il sysfs, montato genericamente in /sys [4], è anch’esso uno pseudo-filesystem. Anch’esso viene utilizzato dal kernel per esportare informazioni allo spazio utente, ma che non hanno niente a che vedere con i processi (es. la dimensione di un disco, il numero di cpu, o informazioni di debug. Inizialmente nei vecchi kernel (serie 2.4) esisteva solo il procfs, che veniva utilizzato come grande contenitore per esportare qualsiasi informazione kernel allo user-space. Successivamente durante lo sviluppo della serie di kernel 2.5 è stato introdotto il sysfs, come metodo più strutturato per separare le informazioni relative ai processi (procfs), rispetto alle informazioni relative ai device o al sistema in genere.

Infine la directory /dev [5] ospita tutti i file speciali che forniscono l’accesso ai device driver, utilizzando le stesse interfacce per l’accesso ai classici (open, read, seek, write, close, etc.). Ad esempio /dev/sda rappresenta il primo di storage rilevato dal sistema, /dev/console il terminale, etc. Anche in questo caso vale il principio dell'”everything is a file”, effettuando ad esempio una read() su /dev/sda, il kernel attiva il device driver che gestisce il dispositivo di storage in questione, e ritorna all’applicazione utente un buffer contenente i dati reperiti sullo storage. Il mapping tra file e device driver in kernel space viene effettuato utilizzando una coppia di numeri detti major e minor number (esistono delle convenzioni sui nomi dei file che si trovano sotto /dev, ma il nome del file dal punto di vista del kernel è totalmente arbitrario).

Per quanto riguarda /proc e /sys è sufficiente creare i mount-point come directory vuote. Dopo che il kernel è partito dovremo predisporre uno script di inizializzazione per montare tali filesystem nei mount-point predisposti.

$ cd /tmp/initrd
$ mkdir proc sys
$ ls -l
total 24
drwxrwxr-x 2 righiandr righiandr 4096 Dec 14 15:07 bin
drwxrwxr-x 2 righiandr righiandr 4096 Dec 14 17:11 proc
drwxrwxr-x 2 righiandr righiandr 4096 Dec 14 15:07 sbin
drwxrwxr-x 2 righiandr righiandr 4096 Dec 14 17:11 sys
drwxrwxr-x 4 righiandr righiandr 4096 Dec 14 15:01 usr

Per /dev, invece è necessario fare un passo aggiuntivo, che consiste nell’inizializzare i file di dispositivo base che il kernel e l’ambiente BusyBox si aspettano di trovare.

Per questo possiamo usare il comando mknod:

$ cd /tmp/initrd
$ mkdir dev
$ cd dev
$ sudo mknod tty1 c 4 1
$ sudo mknod tty2 c 4 2
$ sudo mknod tty3 c 4 3
$ sudo mknod tty4 c 4 4
$ sudo mknod tty5 c 4 5
$ sudo mknod tty6 c 4 6
$ sudo mknod console c 5 1
$ sudo mknod null c 1 3
$ sudo mknod zero c 1 5

I primi sei dispositivi (tty1 … tty6) rappresentano i terminali virtuali (quelli a cui possiamo accedere su un tipico sistema GNU/Linux premendo la combinazione ALT+F1 … ALT+F6); il file /dev/console è il terminale principale. Infine /dev/null e /dev/zero sono device virtuali: il primo, se viene letto ritorna sempre un buffer vuoto, se ci scriviamo sopra i dati vengono ignorati; infine /dev/zero se letto ritorna un buffer pieno di zeri (esattamente una quantità pari alla dimensione del buffer specificato per la lettura).

L’ultimo passo, come visto nella puntata precedente, è di creare nell’initramfs un binario chiamato /init, che il kernel provvederà ad eseguire al termine della fase di boot. Per questo usiamo lo stesso meccanismo di BusyBox: creiamo un link simbolico a /sbin/init, all’interno dell’immagine del nostro initramfs:

$ cd /tmp/initrd
$ ln -s sbin/init init
$ ls -l
$ ls -l
total 24
drwxrwxr-x 2 righiandr righiandr 4096 Dec 15 00:29 bin
drwxrwxr-x 2 righiandr righiandr 4096 Dec 15 00:29 dev
lrwxrwxrwx 1 righiandr righiandr 9 Dec 15 01:25 init -> sbin/init
drwxrwxr-x 2 righiandr righiandr 4096 Dec 15 00:29 proc
drwxrwxr-x 2 righiandr righiandr 4096 Dec 15 00:29 sbin
drwxrwxr-x 2 righiandr righiandr 4096 Dec 15 00:29 sys
drwxrwxr-x 4 righiandr righiandr 4096 Dec 15 00:29 usr

A questo punto abbiamo completato l’immagine di un filesystem GNU/Linux minimale: la stessa base di quello che troviamo in qualsiasi distribuzione GNU/Linux.

Il passo finale è quindi di creare l’initramfs (sempre come abbiamo visto nella puntata precedente) e far partire finalmente la nostra micro-distribuzione:

# cd /tmp/initrd/
# find . | cpio -o –format=newc > ../initramfs
2667 blocks
# du -sh ../initramfs
1.4M ../initramfs

Come possiamo notare la nostra distribuzione occupa solo 1.4MB. Decisamente minimale. :-)

Adesso possiamo far partire la board virtuale su QEMU:

$ qemu-system-arm -M vexpress-a9 -kernel ./arch/arm/boot/zImage -serial stdio -display none -append “console=ttyAMA0″ -initrd /tmp/initramfs
Booting Linux on physical CPU 0
Initializing cgroup subsys cpuset
Linux version 3.5.4 (righiandr@thinkpad) (gcc version 4.7.2 20120910 (prerelease) (crosstool-NG linaro-1.13.1-2012.09-20120921 – Linaro GCC 2012.09) ) #1 SMP Fri Dec 14 14:15:35 CET 2012
CPU: ARMv7 Processor [410fc090] revision 0 (ARMv7), cr=10c53c7d

Freeing init memory: 172K
can’t run ‘/etc/init.d/rcS': No such file or directory

Please press Enter to activate this console.
/ # ls -l
total 0
drwxrwxr-x 2 1000 1000 0 Dec 14 23:29 bin
drwxrwxr-x 2 1000 1000 0 Dec 15 00:28 dev
lrwxrwxrwx 1 1000 1000 9 Dec 15 00:25 init -> sbin/init
drwxrwxr-x 2 1000 1000 0 Dec 14 23:29 proc
drwx—— 2 0 0 0 Dec 14 13:11 root
drwxrwxr-x 2 1000 1000 0 Dec 14 23:29 sbin
drwxrwxr-x 2 1000 1000 0 Dec 14 23:29 sys
drwxrwxr-x 4 1000 1000 0 Dec 14 23:29 usr

E al termine del boot otteniamo finalmente una vera shell sulla nostra board virtuale!

A questo punto possiamo gestire la board come gestiremo un generico sistema GNU/Linux. Possiamo usare il comando “ls” per listare i file del root filesystem (come visto sopra). O montare il procfs e visualizzare la lista dei processi:

/ # mount -t proc none /proc
/ # ps
PID USER TIME COMMAND
1 0 0:01 init
2 0 0:00 [kthreadd]
3 0 0:00 [ksoftirqd/0]
5 0 0:00 [kworker/u:0]
6 0 0:00 [migration/0]
7 0 0:00 [cpuset]
8 0 0:00 [khelper]
9 0 0:00 [kworker/u:1]
158 0 0:00 [sync_supers]
160 0 0:00 [bdi-default]
162 0 0:00 [kblockd]
168 0 0:00 [ata_sff]
178 0 0:00 [khubd]
273 0 0:00 [rpciod]
274 0 0:00 [kworker/0:1]
280 0 0:00 [khungtaskd]
285 0 0:00 [kswapd0]
330 0 0:00 [fsnotify_mark]
339 0 0:00 [nfsiod]
410 0 0:00 [kpsmoused]
413 0 0:00 [kworker/0:2]
463 0 0:00 [deferwq]
468 0 0:00 -/bin/sh
469 0 0:00 init
471 0 0:00 init
472 0 0:00 init
481 0 0:00 ps

Oppure visualizzare il tipo di architettura da /proc/cpuinfo:

/ # cat /proc/cpuinfo
Processor : ARMv7 Processor rev 0 (v7l)
processor : 0
BogoMIPS : 129.43

Features : swp half thumb fastmult vfp edsp neon vfpv3 tls
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xc09
CPU revision : 0

Hardware : ARM-Versatile Express
Revision : 0000
Serial : 0000000000000000

Conclusioni

In questa puntata abbiamo visto come popolare l’initramfs implementando una vera e propria micro-distribuzione GNU/Linux, integrando così quello che avevamo visto nelle puntate precedenti e chiudendo la serie teorica “GNU/Linux partendo da zero”.

Nella prossima puntata analizzeremo più in dettaglio l’aspetto pratico, ripetendo tutti i passi teorici visti nelle puntate precedenti su una board reale: la Raspberry Pi.

Riferimenti

  1. http://www.busybox.net/
  2. http://en.wikipedia.org/wiki/Everything_is_a_file
  3. http://en.wikipedia.org/wiki/Procfs
  4. http://en.wikipedia.org/wiki/Sysfs
  5. http://en.wikipedia.org/wiki/Device_file

 

Quello che hai appena letto è un Articolo Premium reso disponibile affinché potessi valutare la qualità dei nostri contenuti!

 

Gli Articoli Tecnici Premium sono infatti riservati agli abbonati e vengono raccolti mensilmente nella nostra rivista digitale EOS-Book in PDF, ePub e mobi.
volantino eos-book1
Vorresti accedere a tutti gli altri Articoli Premium e fare il download degli EOS-Book? Allora valuta la possibilità di sottoscrivere un abbonamento a partire da € 2,95!
Scopri di più

7 Comments

  1. mneroni 21 gennaio 2013
  2. Emanuele Emanuele 21 gennaio 2013
  3. Boris L. 18 dicembre 2012
  4. Emanuele Emanuele 18 dicembre 2012
  5. aloisius 18 dicembre 2012
  6. Arrigo 25 dicembre 2012
  7. Gianfranco Piroddi 21 febbraio 2015

Leave a Reply