Articoli Manifesto Tools Links Canali Libri Contatti ?
OpenBSD / Kernel

Config ovvero il concepimento del kernel di OpenBSD

Abstract
In questo documento vedremo cosa fa il comando config(8) quando viene eseguito durante la compilazione del kernel di OpenBSD.
In particolare vedremo come vengono interpretate le varie direttive del file di configurazione e come viene gestito l'hardware.

Il testo che segue è valido, quasi totalmente, per tutti i sistemi basati su 4.4BSD

Data di stesura: 30/10/2004
Data di pubblicazione: 08/11/2004
Ultima modifica: 04/04/2006
di Gianluigi Spagnuolo Discuti sul forum   Stampa

Introduzione

Il programma config non fa altro che creare l'occorrente per la compilazione del kernel a partire da un file di configurazione.
Generalmente viene eseguito quando si vuole compilare il kernel nella directory contenente i file di configurazione, che per l'architettura i386 è /sys/arch/i386/conf
# cd /sys/arch/i386/conf
# config ./GENERIC
# cd ../compile/GENERIC
# make depend && make
D'ora in poi ci riferiremo ad un kernel di nome GENERIC compilato per una architettura i386.
Il comando config(8) creerà la directory /sys/arch/i386/compile/GENERIC/, dove GENERIC è il nome del kernel che si sta compilando.
All'interno di tale directory tra i tanti file quelli principali sono il Makefile e "ioconf.c" che tra l'altro contiene la struttura "cfdata", ma questo lo approfondiremo dopo.

Per prima cosa config(8) leggerà il file di configurazione principale, nel nostro esempio "GENERIC", che deve avere come prima riga la direttiva "machine" che indica il tipo di architettura (ad es. "machine i386").
Poi sarà letto il file delle regole indipendente dall'architettura "../../../conf/files" e infine il file delle regole relativo all'architettura scelta, ad esempio "./files.i386"

Di seguito vedremo in dettaglio come vengono gestite da config(8) le varie direttive presenti all'interno del file di configurazione e quale è la funzione e la struttura dei file delle regole.

File di configurazione

Il file di configurazione conterrà, oltre alle indicazioni sul tipo di macchina e processore, le informazioni relative all'hardware che si vuole configurare, le opzioni del kernel e vari altri valori impostabili (maxusers, etc.).

Lo statement "option" viene usato per settare le opzioni del kernel. È possibile usare "option" nelle seguenti forme

option NOME
option NOME=valore
Queste opzioni sono passate al compilatore con il flag -D, ad esempio le righe
option          I686_CPU
option          WSDISPLAY_DEFAULTSCREENS=6
all'interno del file di configurazione, diverranno al momento della compilazione rispettivamente
-DI686_CPU -DWSDISPLAY_DEFAULTSCREENS=6
Lo stesso vale per la direttiva
maxusers        32
che diventa
-DMAXUSERS=32
Questo valore viene usato per le dimensioni di alcune strutture dati del kernel.

I device, invece, sono dichiarati usando la sintassi "figlio at padre", ad esempio

bios0 at mainbus0
Alcuni device forniscono al proprio padre anche delle informazioni necessarie al proprio funzionamento, tali informazioni sono dette "locator"
it0 at isa? port 0x290
Gli pseudodevice sono dichiarati con la parola chiave "pseudo-device" seguita dal nome e dal numero di istanze di quel particolare device
pseudo-device   crypto  1
pseudo-device   wsmux   2
Il significato di queste dichiarazioni sarà spiegato in dettaglio nei prossimi paragrafi.

I file generati da config(8) popoleranno la directory "../compile/GENERIC" In tale directory sarà presente anche un link simbolico a "../../../../arch/i386/include" di nome "machine" ed un link a ""machine" di nome "i386" per permettere l'utilizzo di include del tipo

#include <machine/vparam.h>

Device e files

Innanzitutto è necessaria una breve parentesi sui device all'interno del kernel di OpenBSD.
Il kernel gestisce i device come un albero gerarchico, dove ogni singolo device si attacca al device padre. Non ci sono restrizioni sul numero di figli che un device può avere, ed è possibile anche avere più padri.
In cima alla gerarchia c'è lo pseudo-device "mainbus" che risulta esso stesso attaccato al nodo fittizio "root". Indicativamente avremo una struttura del genere:
                    eisa--
                  /     
                 /      /
root-----mainbus ----isa --mcd
                 \
                  \     /
                    pci --
                        \
Le direttive relative ai device sono gestite da config(8) con l'ausilio dei file delle regole, denominati files.*, sparsi per il kernel.
config(8) usa tali file per creare il contenuto della directory di compilazione.
I file delle regole contengono le informazioni necessarie per fare il parsing del file di configurazione; in pratica i files.* forniscono, per ogni device e pseudo-device, varie informazioni sulla loro gestione.
In particolare indicano a quale device o bus si devono attaccare, quali attributi e locator sono presenti, quali file includere e quali header creare.

Attributi e locator

Come abbiamo visto prima i locator servono a passare informazioni dal figlio al padre, assumono come valore un intero o una stringa che viene risolta a intero.
Oltre ai locator è possibile definire anche degli attributi, che indicano le proprietà del device. All'interno dei file delle regole sono definiti con la parola chiave "define" nella seguente forma
define attributo
oppure se si tratta di un attributo di tipo attachment è possibile anche indicare i suoi locator, ad esempio
define pckbcport { [irq = -1], [port = -1]}
Ogni attributo può definire uno o più locator. È possibile fare riferimento agli attributi attraverso la direttiva "device".

Nel file di configurazione è possibile fare riferimento solo ai locator indicati nel file delle regole, e solo a quelli, ad esempio se nel file files.isa è indicato

device  isa {[port = -1], [size = 0], [iomem = -1], [iosiz = 0],
             [irq = -1], [drq = -1], [drq2 = -1]}
all'interno del file di configurazione è valida la seguente linea
fdc0    at isa? port 0x3f0 irq 6 drq 2
mentre non lo è quest'altra
fdc0    at isa? port 0x3f0 pluto ?
È possibile anche assegnare dei valori di default ai locator, in genere si usa 0 per gli indirizzi e -1 per gli indici e i numeri di drive.
Se un device ha un "?" come valore per un locator allora config(8) userà il valore di default. Inoltre se il locator è scritto tra [] può essere completamente omesso nella definizione del device.
Il "?" riferito al padre sta a indicare che il figlio può legarsi a tutti i padri di quel tipo istanziati, incluso i cloni.

Esempio e flag

Vediamo adesso un esempio reale di gestione di un device da parte del kernel, prendiamo in considerazione il driver dei CD-ROM Mitsumi. Nel file di configurazione del kernel per attivare il supporto a tale periferica basta solo la seguente riga
mcd0 at isa? port 0x300 irq 10
dove "port" e "irq" sono due locator di "isa" definiti nel file files.isa come visto in precedenza.
In "sys/dev/isa/files.isa" troviamo le seguenti righe:
device  mcd: disk, opti
attach  mcd at isa
file    dev/isa/mcd.c   mcd needs-flag
la prima linea comunica a config(8) l'esistenza di un driver chiamato "mcd" con gli attributi "disk" e "opti". La seconda linea indica che "mcd" si lega al bus "isa", mentre la terza riga dice che il file "dev/isa/mcd.c" deve essere compilato nel kernel se tale device è inserito nel file di configurazione e se le dipendenze, indicate dopo il nome del file, sono soddisfatte.
"needs-flag" sta a indicare che deve essere creato il file "mcd.h" nella directory di compilazione, che conterrà la riga
#define NMCD    1
se il driver mcd deve essere presente nel kernel,
#define NMCD    0
in caso contrario.

config(8) creerà un makefile che contiene regole di compilazione diverse per ogni file, quelli con estensione .c useranno la regola generica ${NORMAL_C}.
Per usare, invece, una regola diversa da quella standard bisogna usare il comando "compile-with" alla fine della riga "file".

Altre etichette, oltre a "need-flag" e "compile-with", che possono essere usate con i file sono:

device-driver equivale a compile-with"${DRIVER_C}"
config-dependent il file è compilato con la regola ${NORMAL_C_C} oppure con ${DRIVER_C_C}
needs-count indica al momento della compilazione quante istanze sono presenti. Ad esempio una riga del tipo file net/bpf.c bpfilter needs-count genererà un header file "bpfilter.h" contenente la riga "#define NBPFILTER 8"

Pseudo-device

Gli pseudo-device sono definiti allo stesso modo dei device veri e propri, con la differenza che non si possono usare i locator
pseudo-device nome: attributi
Avremo ad esempio nel file sys/conf/files
pseudo-device pf: ifnet
file    net/pf.c                pf      needs-flag
file    net/pf_norm.c           pf
file    net/pf_ioctl.c          pf
file    net/pf_table.c          pf
file    net/pf_osfp.c           pf
anche in questo caso, come per i device, possiamo vedere gli attributi, i file da includere, le dipendenze e la regola need-flag.

Device e cfdata

Le relazioni tra i driver e i bus, tra padri e figli, e numerose altre informazioni sono contenute in un array di strutture "cfdata", di nome "cfdata", all'interno del file "ioconf.c" nella directory di compilazione.
La struttura "cfdata" è definita nel file "/sys/sys/device.h"
  1. struct cfdata { 
  2.   struct  cfattach *cf_attach;     
  3.   struct  cfdriver *cf_driver; 
  4.   short   cf_unit; 
  5.   short   cf_fstate; 
  6.   int     *cf_loc; 
  7.   int     cf_flags; 
  8.   short   *cf_parents; 
  9.   int     cf_locnames; 
  10.   void    (**cf_ivstubs)(void); 
  11.   short   cf_starunit1; 
  12. }; 
Brevemente abbiamo che "cf_fstate" può assumere i seguenti valori: FSTATE_NOTFOUND, FSTATE_FOUND, FSTATE_STAR (clone), FSTATE_DNOTFOUND e FSTATE_DSTAR (la "D" sta per "disabilitato").
"cf_ivstubs" gestisce le interruzioni, "cf_unit" e "cf_loc" rappresentano rispettivamente il numero di unità e un puntatore al primo locator specifico del device.
"cf_parents" è un puntatore ad un eventuale padre, "cf_flags" indica i flag dal file di configurazione e "cf_startunit1" indica il primo numero di unità disponibile.

Le strutture "cfattach" e "cfdriver" sono definite nello stesso file nel seguente modo:

  1. struct cfattach { 
  2.   size_t    ca_devsize;     
  3.   cfmatch_t ca_match;   
  4.   void    (*ca_attach)(struct device *, struct device *, void *); 
  5.   int     (*ca_detach)(struct device *, int); 
  6.   int     (*ca_activate)(struct device *, enum devact); 
  7.   void    (*ca_zeroref)(struct device *); 
  8. }; 
  9.  
  10. struct cfdriver { 
  11.   void    **cd_devs; 
  12.   char    *cd_name; 
  13.   enum    devclass cd_class; 
  14.   int     cd_indirect; 
  15.   int     cd_ndevs; 
  16. }; 
"cf_attach" rappresenta l'interfaccia tra il driver e il sistema, convenzionalmente per ogni device il nome di tale struttura è composto dal nome del device seguito da "_ca", ad esempio "rl_pci_ca".
La struttura "cf_attach" fornisce le funzioni match, attach, detach e activate per ogni device.
Inoltre il campo "ca_devsize" rappresenta la quantità di memoria che il framework per l'autoconfigurazione deve allocare ad ogni nuova istanza del driver. Tale dimensione si riferisce alla struttura "softc" del driver che conterrà le variabili relative allo specifico driver. In questo caso il nome è dato dal nome del device seguito da "_softc".

Ogni struttura "cfdata" fa riferimento al proprio driver attraverso un puntatore a "struct cfdriver". In questo caso il nome della struttura è ottenuto aggiungendo "_cd" al nome del device, "rl_cd".
Tutti i campi, tranne "cd_devs" e "cd_ndevs", possono solo essere letti dal sistema e non modificati.
"cf_driver" indica il nome del device e la classe. "cf_devs" è un puntatore all'array di driver del device istanziati, e "cd_ndevs" indica le dimensioni di tale array.
I vari tipi di classe sono elencati in "sys/sys/device.h"

  1. enum devclass { 
  2.   DV_DULL, 
  3.   DV_CPU, 
  4.   DV_DISK, 
  5.   DV_IFNET, 
  6.   DV_TAPE, 
  7.   DV_TTY 
  8. }; 
da notare la classe "dull" che conterrà tutti i device che non rientrano nelle altre classi.

Una tipica entry di cfdata è la seguente

  1. struct cfdata cfdata[] = { 
  2.   ... 
  3.   /* 37: rl* at pci* dev -1 function -1 */ 
  4.   {&rl_pci_ca, &rl_cd,   0, STAR, loc+205, 0, pv+101, 30, 0, 0} 
  5.   ... 
  6. }; 
con "rl_pci_ca" e "rl_cd" che valgono rispettivamente
  1. struct cfattach rl_pci_ca = { 
  2.   sizeof(struct rl_softc), rl_pci_match, rl_pci_attach, 
  3. }; 
  4.  
  5. struct cfdriver rl_cd = { 
  6.   0, "rl", DV_IFNET 
  7. }; 
Oltre a "cfdata" nel file "ioconf.c" ci sono informazioni relative agli pseudo-device, ai locator, ai parent vector, etc.

Informazioni sull'autore

Gianluigi Spagnuolo, studente. Si interessa di programmazione, sicurezza informatica, buddismo zen, reti e sistemi operativi liberi.
È tra i fondatori dell' Italian Ruby User Group e dell'Irpinia Linux User Group e moderatore della mailing list Security4Dummies.
Home page: http://kirash.interfree.it

È possibile consultare l'elenco degli articoli scritti da Gianluigi Spagnuolo.

Altri articoli sul tema OpenBSD / Kernel.

Risorse

  1. man config(8)
  2. man files.conf(5)
  3. man autoconf(9)
  4. S.J.Leffer, M.J.Karels - "Building Berkeley UNIX Kernels with Config"
    4.3BSD UNIX System Manager's Manual
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.