Contr. Sociale Copertina Linux in Viaggio |
Articoli
I formati binari
Ristampato con il permesso del Linux Journal
Una delle potenzialità implementabili tramite moduli del kernel è l'aggiunta a run-time di nuovi formati binari. Un ``formato binario'' è fondamentalmente la struttura dati che definisce come eseguire i file di programma (quelli con il permesso di esecuzione attivato). Questo articolo spiega come è possibile aggiungere un nuovo formato binario al sistema mediante i moduli, accompagnando la spiegazione con un paio di esempi. Il codice d'esempio richiede la versione 2.x del kernel.
Come è noto i moduli del kernel sono stati introdotti per aggiungere
nuove funzionalità ad un sistema Linux, in particolare i device
driver del kernel possono essere compilati come moduli per essere caricati
a run-time (manualmente o tramite il kerneld).
In realtà, il modello estremamente modulare del kernel di Linux,
permette di aggiungere a run-time molte altre funzionalità oltre ai
device driver (ad esempio i file system possono essere implementati sotto
forma di modulo, come si è visto nel numero di ottobre a proposito
dei file /proc
e dei punti di accesso a sysctl
).
Un'altra funzionalità caricabile come modulo è la capacità di
eseguire i formati binari, includendo in ciò sia i file eseguibili
sia le librerie dinamiche. Mentre il meccanismo di caricamento dei
programmi compilati e delle librerie dinamiche è particolarmente
elaborato, l'utente medio di Linux può implementare facilmente un
loader che richiami un interprete per nuovo formato binario, in modo
da riuscire a chiamare per nome un file di dati ed vederne
l'``esecuzione'', dopo avere chiamato chmod +x
per il file
stesso.
Introducioamo l'argomento, spiegando come è implementata in Linux la
chiamata di sistema exec
. Questa è una delle parti più
interessanti del kernel, poiché la possibilità di eseguire i
programmi è alla base del funzionamento del sistema operativo.
Il codice della chiamata di sistema exec
risiede nella parte
dei sorgenti del kernel che dipende dall'architettura usata, ma tutto
il codice interessante è contenuto in fs/exec.c
(tutti i
percorsi sono riferiti a /usr/src/linux
, o comunque alla
directory dei sorgenti del kernel). Chi volesse verificare i dettagli
specifici ad ogni architettura può individuare la funzione relativa
tramite il comando: grep -n 'asmlinkage.*exec'
arch/*/kernel/*.c
.
All'interno di fs/exec.c
la funzione che racchiude la
funzionalità di exec
è do_execve
, composta da meno di
cinquanta linee di codice. Il suo ruolo è controllare gli errori,
riempire la struttura ``binary parameter'' (struct linux_binprm
)
e cercare il gestore del formato del file che viene eseguito. L'ultimo
passo è compiuto da search_binary_handler
, un'altra funzione
contenuta nello stesso file.
La ``magia'' di do_exec
è contenuta in quest'ultima funzione,
che è molto corta: il suo lavoro consiste nello scandire l'elenco dei
formati binari noti al sistema, passando la struttura binprm
a
ciascuno di essi finché non viene accettata. Se nessun gestore è in
grado di gestire il file eseguibile, il kernel cerca di caricare un
nuovo gestore tramite kerneld
, dopo di che scandisce l'elenco
ancora una volta. Se nessun formato binario è in grado di lanciare
il file eseguibile, la chiamata di sistema restituisce il codice di
errore ENOEXEC
(``Exec format error'').
Il principale problema con questo tipo di implementazione è quello di
mantenere Linux compatibile con il comportamento standard di Unix:
ogni file di testo eseguibile che inizi con #!
deve essere
eseguito dall'interprete richiesto, mentre tutti gli altri testi
eseguibili devono essere interpretati da /bin/sh
. Il primo
problema è gestito mediante un semplice formato binario specializzato
nell'esecuzione dei file interpretati (fs/binfmt_script.c
), e
l'interprete stesso è eseguito chiamando
search_binary_handler()
ancora una volta; la funzione è
progettata per essere rientrante, e binfmt_script
controlla di
non venire invocato due volte. Il secondo problema è principalmente
un ``refuso storico'' ed è semplicemente ignorato dal kernel, e sarà
il programma utente che cerca di eseguire il file che dovrà tenerne
conto. Tale programma in genere è una shell interattiva, oppure il
comando make
. È interessante notare che mentre le versioni
recenti di gmake
gestiscono correttamente gli script senza
#!
, le versioni precedenti non richiamano la shell in questo
caso, e può suggeredere di ricevere il messaggio ``cannot execute
binary file'' quando si invocano script senza il #!
dall'interno
di un Makefile.
Tutto il lavoro di gestione delle strutture dati necessarie per
sostituire la vecchia immagine dell'eseguibile con la nuova è
compiuta dallo specifico gestore per il formato binario, appoggiandosi
su funzioni di utilità esportate dal kernel. Consiglio a chi vuole
curiosare nel codice di iniziare con la funzione
load_out_binary()
contenuta nel file fs/binfmt_aout.c
;
tale funzione è molto più facile del corrispondente gestore per il
formato ELF.
L'implementazione di exec
è codice abbastanza interessante, ma
Linux ha molto di più da offrire: la registrazione di un nuovo
formato binario a run-time. L'implementazione è piuttosto semplice,
benché coinvolga strutture dati piuttosto elaborate---o il codice o
le strutture dati devono tenere conto della complessità di fondo;
gestire la complessità nelle strutture dati offre una maggior
flessibilità che una soluzione basata su codice complesso. L'essenza
di un formato binario è rappresentato nel kernel dalla struttura
chiamata struct linux_binfmt
, definita in
linux/binfmts.h
nel seguente modo:
struct linux_binfmt {
struct linux_binfmt *next;
long *use_count;
int (*load_binary)(struct linux_binprm *, struct pt_regs *);
int (*load_shlib)(int fd);
int (*core_dump)(long signr, struct pt_regs *);
};
Le tre funzioni, o ``metodi'', dichiarate dal formato binario servono ad
eseguire un file eseguibile, a caricare una libreria dinamica e a
creare un file di core. Il puntatore next
è usato dalla
funzione search_binary_handler()
, mentre il puntatore
use_count
aiuta a tenere traccia dell'utilizzo dei moduli: ogni
qualvolta un processo p
è eseguito da un formato binario
modularizzato, il kernel utilizza *(p->binfmt->use_count)
per
prevenire la rimozione inaspettata del modulo.
Un modulo, poi, utilizza le funzioni seguenti caricare e scaricare
se stesso:
extern int register_binfmt(struct linux_binfmt *);
extern int unregister_binfmt(struct linux_binfmt *);
Le funzioni ricevono un solo argomento invece della solita coppia
puntatore-e-nome, poiché nessun file in /proc
elenca i formati binari
disponibili, a differenza di quello che accade per la maggior parte
delle risorse di sistema (per cui, non occorre il nome del formato).
Il tipico codice per caricare e scaricare un formato binario, perciò,
è semplice come questo:
int init_module (void) {
return register_binfmt(&bluff_format);
}
void cleanup_module(void) {
unregister_binfmt(&bluff_format);
}
Queste poche righe di codice appartengono al modulo bluff
(Binary Loader for an Ultimately Fallacious Format), il cui sorgente
è disponibile qui (oppure all'interno
del tar con tutti i sorgenti, come descritto in fondo).
La struttura che rappresenta il formato binario può dichiarare le
funzioni che offre come NULL
: le funzioni poste a NULL
saranno ignorate dal kernel. Il formato binario più semplice,
perciò, somiglierà al seguente:
struct linux_binfmt bluff_format = {
NULL, &mod_use_count_, /* next, count */
NULL, NULL, NULL /* bin, lib, core */
};
Sì, come vedete bluff
è un bluff: è possibile
caricarlo e scaricarlo ogni volta che si vuole, ma non fa
assolutamente nulla.
Per riuscire a implementare un formato binario con un minimo di
funzionalità, il programmatore deve avere qualche nozione a proposito
degli argomenti passati alla funzione di caricamento, cioè
format->load_binary
: il primo di tali argomenti contiene la
descrizione del file binario, i ``parametri''; il secondo argomento è
un puntatore ai registri del microprocessore.
Il secondo argomento è necessario solo per i loader binari
reali, come i formati a.out e ELF che potete trovare nei
sorgenti del kernel. Quando il kernel sostituisce un file eseguibile
con uno nuovo, deve inizializzare in modo corretto i registri
associati al processo corrente; in particolare, il puntatore
all'istruzione (Instruction Pointer, IP) deve essere impostato
all'indirizzo dove deve cominciare l'esecuzione del nuovo
programma. La funzione start_thread
è esportata dal kernel per
semplificare tale l'impostazione del puntatore all'istruzione. In
questo articolo non si andrà nei dettagli dei loader reali, la
discussione sarà invece limitata ai formati ``wrapper'', come
binfmt_script
e binfmt_java
.
La struttura linux_binprm
, però, deve essere utilizzata anche
per i gestori più semplici, ed occorre quindi desciverla.
La struttura contiene i seguenti campi:
char buf[128]
: questo buffer contiene i primi byte
dell'immagine eseguibile. Solitamente viene consultata da ogni
gestore di formato binario per scoprire il tipo di
file. Volendo curiosare tra i ``numeri magici'' noti utilizzati
per identificare i diversi formati dei file, è possibile
consulare il file /usr/lib/magic
(a volte chiamato
/etc/magic
).
unsigned long page[MAX_ARG_PAGES]
: questo vettore contiene
gli indirizzi delle pagine dati usate per esportare al nuovo
programma le variabili di ambiente (environment) e l'elenco
degli argomenti che gli vengono passati. Le pagine sono
allocate solo quando realmente usate: la memoria non viene
sprecata quando le variabili di ambiente e l'elenco degli
argomenti sono ridotti. La macro MAX_ARG_PAGES
è
dichiarata in nell'header binfmts.h
ed è attualmente
impostata a 32 (128kB, 256kB sull'Alpha). Se si ottiene il
messaggio ``Arg list too long'' cercando di eseguire un grep
massiccio, allora è necessario aumentare il valore di
MAX_ARG_PAGES
e ricompilare il kernel (o ridurre la
linea di comando).
unsigned long p
: questo è un ``puntatore'' ai dati
contenuti nella pagine precedentemente descritte: i dati sono
inseriti nelle pagine partendo dagli indirizzi alti verso
quelli bassi, e p
punta sempre all'inizio dei dati. I
formati binari possono usare il puntatore per utilizzare gli
argomenti iniziali che vengono passati al programma (nella
prossima sezione vedremo come fare). È interessante notare
che p
è un puntatore ad indirizzi nello spazio utente;
è dichiarato come un unsigned long
per evitare
indesiderate dereferenziazioni del suo valore. Quando un
indirizzo rappresenta dati generici (o un offset nella
memoria, considerata vettore di byte) il kernel lo considera
spesso un unsigned long
.
struct inode *inode
: è l'i-node che rappresenta il file
da eseguire.
int e_uid, e_gid
: sono gli user id e group id effettivi
del processo che esegue il programma. Se il programma è
set-uid, questi campi rappresentano i nuovi valori.
int argc, envc
: rappresentano il numero di argomenti passati
al nuovo programma ed il numero delle variabili di ambiente.
char *filename
: è il pathname completo del programma da
eseguire. Questa stringa risiede nello spazio del kernel, ed
è il primo argomento ricevuto dalla chiamata di sistema
execve
. Benché il programma utente non avrà la
necessità di conoscere il suo percorso completo,
l'informazione è disponibile ai formati binari, in modo che
questi possano fare cose strane con la lista degli argomenti.
int dont_iput
: è un flag che può essere impostato dal
formato binario per informare il livello superiore che
l'i-node è stato rilasciato dallo stesso loader.
La struttura include altri campi che però non interessano per la
realizzazione di formati binari semplici. Quello che è rilevante,
d'altra parte, sono un paio di funzioni esportate da exec.c
. Le
funzioni sono state ideate per semplificare il lavoro di semplici
gestori di formati binari, come quello che verrà introdotto di
seguito. Le funzioni sono:
unsigned long copy_strings(int argc,char ** argv,
unsigned long *page, unsigned long p, int from_kmem);
void remove_arg_zero(struct linux_binprm *bprm);
La prima funzione si occupa di copiare argc
stringhe dal
vettore argv
all'interno del puntatore p
(un puntatore
allo spazio utente, di solito bprm-> p
). Le stringhe saranno
copiate davanti al puntatore p
. Le stringhe originali, quelle
contenute in argv
, possono risiedere sia nello spazio utente
che nello spazio del kernel, e il vettore argv
stesso può
risiedere nello spazio del kernel, anche se le stringhe sono
memorizzate nello spazio utente. L'argomento from_kmem
è
utilizzato per specificare se le stringhe da copiare ed il vettore
risiedono entrambi nello spazio utente (0), entrambi nello spazio del
kernel (2), o se le stringhe sono nello spazio utente e il vettore
nello spazio del kernel (1). remove_arg_zero
è molto più
semplice: si limita a rimuovere il primo argomento da bprm
incrementando bprm->p
.
Per mettere in pratica quanto appreso nella teoria, cerchiamo di
espandere il nostro bluff
(visto in precedenza) in un
bloom
(Binary Loader for Outrageously Ostentatious Modules). Il
sorgente completo del nuovo modulo è distribuito insieme a bluff.
Il ruolo di bloom è visualizzare le immagini eseguibili. È
sufficiente dare il permesso di esecuzione ai file GIF e caricare il
modulo: chiamando le immagini per nome, verrà eseguito xv
per
visualizzarle.
Questo codice non è particolarmente originale (la maggior parte di
esso viene da binfmt_script.c
) e non è particolarmente
elegante (gli utenti solo-testo come me preferiscono utilizzare
visualizzatori ASCII e gli altri utenti potranno preferire un
visualizzatore diverso). L'esempio è principalmente a fine didattico
e può essere facilmente utilizzato da chiunque disponga di un sistema
X - e possa accedere al computer come root per caricare il modulo.
Il file sorgente è costituito da meno di 50 linee, ed è in grado di
eseguire GIF, TIFF ed i vari formati PBM; è inutile dire che è
necessario eseguire preventivamente chmod +x
sulle immagini. La
scelta del visualizzatore si può fare quando si carica il modulo; il
visualizzatore di default è usr/bin/X11/xv
. Di seguito è
riportata una semplice sessione effettuata sulla mia console di testo:
Utilizzando il visualizzatore di default e lavorando in una sessione
grafica, l'immagine ``fiorirà'' sul display.
morgana.root# insmod bloom.o
morgana.root# ./snowy.tif
xv: Can't open display
morgana.root# rmmod bloom
morgana.root# insmod bloom.o viewer="/bin/cat"
morgana.root# ./snowy.tif | wc -c
1067564
Chi non volesse attendere di scaricare il codice sorgente può vedere
comunque la parte saliente di bloom in
questo listato. Si noti che
bloom.c
è distribuito secondo la licenza GPL, poiché la
maggior parte di suo codice è derivato da binfmt_script.c
.
La domanda che sorge naturale è ``come è possibile configurare il
sistema in modo che kerneld
gestisca in modo automatico il
modulo del nuovo formato binario?''
Purtroppo, non sempre è possibile. Il codice in fs/exec.c
tenta di utilizzare kerneld solo quando almeno uno dei primi 4 byte
non è stampabile. Questo comportamento è stato pensato per evitare
di perdere molto tempo con kerneld quando il file da eseguire è un
file di testo senza la riga ``#!
''. Benché i file compilati abbiano un
byte non stampabile nei primi quattro, questo non è sempre vero per
tipi di dati generici.
L'immediata conseguenza di questo comportamento è l'impossibilità di
caricare automaticamente il visualizzatore bloom
richiamando
file GIF o PBM per nome. Entrambi i formati cominciano con una
sequenza di testo, e saranno quindi ignorati dall'auto-loader.
Quando, invece, il file ha un carattere non stampabile fra i primi
quattro, allora il kernel emette una richiesta al kerneld per un
module ``binfmt-
numero'', dove la sequenza esatta è generata
da questa riga:
sprintf(modname, "binfmt-%hd", *(short*)(&bprm->buf));
L'ID del formato binario, perciò, rappresenta i primi due byte del
file su disco. Tentando di eseguire file TIFF, il kerneld proverà a
cercare ``binfmt-19789
'' o ``binfmt-18761
'', un file
compresso con gzip chiamerà ``binfmt--29921
'' (negativo), ed i
file GIF saranno passati direttamente a /bin/sh
, poiché
iniziano con una stringa ti testo ASCII. Volendo conoscere il numero
associato con ciascun formato binario è sufficiente riferirsi a
/usr/lib/magic
e convertire i valori in decimali; in
alternativa, è possibile passare l'argomento debug
a kerneld e
guardare i messaggi che esso genera quando si tenta di caricare il
formato binario necessario per eseguire un file dati.
È interessante notare che la versione 2.1.23 e le più recenti hanno
cambiato il meccanismo ed utilizzano un ID più semplice e più
significativo, usando la linea seguente per generarlo:
sprintf(modname, "binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));
Questa nuova stringa ID rappresenta il terzo e il quarto byte del file binario ed è esadecimale invece di decimale (così da ottenere stringhe con un formato migliore evitando il caso del duplice meno, come succede per la stringa ``binfmt--29921'' visto in precedenza).
Benché chiamare le immagini con il loro nome possa essere divertente, questa funzionalità non ha alcun bisogno di esistere in un sistema operativo. Personalmente preferisco chiamare il mio visualizzatore per nome, piuttosto che essere preso in giro da un sistema che finga di fare tutto da solo---e non credo neppure alla ``orientazione agli oggetti'' di questo tipo di approccio. Questo genere di funzionalità secondo me si adatta meglio ad un file manager, dove una corretta configurazione può gestire le cose senza introdurre alcun appesantimento al kernel (che andrebbe sempre a scapito della velocità computazionale).
Quello che realmente risulta interessante dei formati binari è
la possibilità di eseguire file di programma che non ricadono nella
comoda notazione ``#!
''. Questo include tutti i file eseguibili che
appartengono ad altri sistemi operativi o altre piattaforme, come pure
tutti i linguaggi interpretati che non sono stati progettati per il
sistema operativo Unix -- tutti quei linguaggi che si lamenterebbero
di un ``#!
'' nella prima linea.
Chi vuole giocare con una di queste possibilità può provare il
modulo fail, il cui sorgente è distribuito con bloom (si veda
il link al termine dell'articolo). Questo ``Format for Automatically
Interpreting Lisp'' è un wrapper per richiamare emacs
ogni
volta che viene che un programma e-lisp compilato viene chiamato per
nome (a patto che sia stato reso eseguibile). Tale pratica è
decisamente poco funzionale, in quanto fa una certa impressione
richiamare un programma di diversi megabyte di codic per eseguire solo
alcune linee di lisp (c'è da dire, però che anche Java funziona
così...). Altro svantaggio di fail
è che emacs-lisp non è
adatto all'utilizzo da linea di comando; ad ogni caso fail
può
essere una cosa divertente da provare. Insieme con fail
si
trovano anche un paio di eseguibili in lisp per provare.
Un sistema Linux completo è pieno di esempi interessanti di formati
binari interpretati: il formato binario Java è uno di questi, la
piattaforma Alpha è in grado di eseguire programmi per Linux-x86
utilizzando uno speciale formato binario, e le più recenti
distribuzioni di dosemu
sono in grado di eseguire i vecchi
programmi DOS in modo trasparente (benché il programma debba essere
configurato preventivamente in modo opportuno).
La versione 2.1.43 del kernel e le successive includono un supporto
generico per i formati binari interpretati: binfmt_misc
è
simile a bloom
, ma è molto più potente. Si possono facilmente
aggiungere al modulo nuovi formati binari interpretati semplicemente
scrivendo le informazioni opportune nel file
/proc/sys/fs/binfmt_misc
.
Tutti i programmi di esempio citati in questo articolo si trovano in questo tar compresso.
di Alessandro Rubini, traduzione di Andrea Mauro
Contr. Sociale Copertina Linux in Viaggio |