Un problema che si presenta comunemente nello sviluppo dei programmi è che questi tendono a diventare sempre più complessi, il tempo richiesto per la loro compilazione cresce di conseguenza, e la directory di lavoro è sempre più affollata. E' proprio in questa fase che incominciamo a chiederci se non esista un modo più efficiente per organizzare i nostri progetti. Una possibilità che ci viene offerta dai compilatori sono le librerie.
Una libreria è semplicemente un file contenente codice compilato che può essere successivamente incorporato come una unica entità in un nostro programma in fase di linking; l'utilizzo delle librerie ci permettere di realizzare programmi più facili da compilare e mantenere. Di norma le librerie sono indicizzate, così risulta più facile localizzare simboli (funzioni, variabili, classi, etc...) al loro interno. Per questa ragione il link ad una libreria è più veloce rispetto al caso in cui i moduli oggetto siano separati nel disco. Inoltre, quando usiamo una libreria abbiamo meno files da aprire e controllare, e questo comporta un ulteriore aumento della velocità del processo di link.
Nell'ambiente Linux (come nella maggior parte dei sistemi moderni) le librerie si suddividono in due famiglie principali:
- librerie statiche (static libraries)
- librerie dinamiche o condivise (shared libraries)
Ognuna presenta vantaggi e svantaggi, ma tutte hanno una cosa in comune: costituiscono un catalogo di funzioni, classi, etc..., che ogni programmatore può riutilizzare.
Prima di vedere come si costruiscono e si usano questi due tipi di librerie, presentiamo un piccolo programma di prova che ci servirà da esempio.
Il programma comprende una collezione di funzioni matematiche (myfuncs) ed un gestore di errori (la classe ErrMsg):
Le funzioni ''div'' e ''log'' in sostanza ridefiniscono le operazioni di divisione e il logaritmo decimale ma in aggiunta permettono una gestione delle eccezioni tramite il meccanismo di throw-catch.
Il programma può essere compilato in maniera ''convenzionale'' tramite l'istruzione:
g++ -o prova main.cpp myfuncs.cpp errmsg.cppL'eseguibile prova si aspetta sulla linea di comando due numeri e calcola in sequenza il loro rapporto ed il logaritmo del primo:
./prova 10 3 3.33333 1
Queste operazioni vengono eseguite nel main del programma
in un blocco try; se si verifica una eccezione (nella fattispecie
una divisione per zero o il logaritmo di un numero negativo) il blocco catch
invoca la funzione membro ErrMsg.print_message() ed il programma
termina con un messaggio di errore:
Le librerie statiche vengono installate nell'eseguibile del
programma prima che questo possa essere lanciato. Esse sono
semplicemente cataloghi di moduli oggetto che sono stati collezionati in
un unico file contenitore. Le librerie statiche ci permettono di effettuare
dei link di programmi senza dover ricompilare il loro codice sorgente. Per
far girare il nostro programma abbiamo bisogno solo del suo file
eseguibile.
Una volta compilati i moduli myfuncs.o
e errmsg.o, costruiamo la libreria statica libmath_util.a con
il programma di archiviazione ar:
Il comando ar invocato con la flag
''r'' crea la libreria (se ancora non esiste) e vi inserisce
(eventualmente rimpiazzandoli) i moduli oggetto. Nel
scegliere il nome di una libreria statica è stata utilizzata la seguente
convenzione: il nome del file della libreria inizia con il prefisso
''lib'' e termina con il suffisso ".a".
Per verificare il contenuto della libreria
possiamo usare
Una volta creato il nostro archivio, vogliamo
utilizzarlo in un programma. Per poter effettuare il link ad una libreria
statica, il compilatore g++ deve essere utilizzato in questo modo:
Dove abbiamo chiamato l'eseguibile
prova_s per ricordarci che è stato ottenuto tramite il link
alla libreria statica. Notate che abbiamo omesso il prefisso ''lib''
e il suffisso ''.a'' quando abbiamo immesso il nome della libreria
nella linea di comando con la flag "-l". Ci pensa il linker ad attaccare
queste parti alla fine e all'inizio del nome di libreria. Notate inoltre
l'uso della flag ''-L.'' che
dice al compilatore di cercare la libreria anche nella directory in uso e
non solo nelle directory standard dove risiedono le librerie di sistema (per
es. /usr/lib/).
Il processo di link inizia con il caricamento
del modulo main.o in cui viene definita la funzione main(). A
questo punto il linker si accorge della presenza dei nomi di funzioni
div e log e della classe ErrMsg, utilizzate dalla funzione
main() ma non definite. Siccome viene fornito al linker il nome della
libreria libmath_util.a, viene fatta una ricerca nei moduli all'interno
di questa libreria per cercare quelli in cui sono definite queste entità.
Una volta localizzati, questi moduli vengono estratti dalla libreria ed inclusi
nell'eseguibile del programma.
L'eseguibile prova_s contiene così
tutto il codice necessario al suo funzionamento ed è pronto per essere
lanciato.
Si deve precisare che il linker estrae dalla
libreria statica solo i moduli strettamente necessari alla compilazione del
programma. Questo dimostra una certa capacità di economizzare le risorse
delle librerie. Pensiamo però a più programmi che utilizzano,
magari per altri scopi, la stessa libreria statica. I programmi utilizzano
la libreria statica distintamente, cioè ognuno ne possiede una copia.
Se questi devono essere eseguiti contemporaneamente nello stesso sistema,
i requisiti di memoria si moltiplicano di conseguenza solo per ospitare funzioni
assolutamente identiche.
Le librerie condivise forniscono un meccanismo
che permette a una singola copia di un modulo di codice di essere condivisa
tra diversi programmi nello stesso sistema operativo. Ciò permette
di tenere solo una copia di una data libreria in memoria ad un certo
istante.
Le librerie condivise (dette anche dinamiche)
vengono collegate ad un programma in due passaggi. In un primo momento, durante
la fase di compilazione (Compile Time), il linker verifica che tutti
i simboli (funzioni, variabili, classi, e simili ...) richieste dal programma
siano effettivamente collegate o al programma o ad una delle sue librerie
condivise. In ogni caso i moduli oggetto della libreria dinamica non
vengono inseriti direttamente nel file eseguibile. In un secondo
momento, quando l'eseguibile viene lanciato (Run Time), un programma
di sistema (dynamic loader) controlla quali librerie dinamiche sono
state collegate al nostro programma, le carica in memoria, e le attacca alla
copia del programma in memoria.
La fase di caricamento dinamico rallenta
leggermente il lancio del programma, ma si ottiene il notevole vantaggio
che, se un secondo programma collegato alla stessa libreria condivisa viene
lanciato, questo può utilizzare la stessa copia della libreria dinamica
già in memoria, con un prezioso risparmio delle risorse del sistema.
Per esempio, le librerie standard del C e del C++ sono delle librerie condivise
utilizzate da tutti i programmi C/C++.
L'uso di librerie condivise ci permette quindi
di utilizzare meno memoria per far girare i nostri programmi e di avere
eseguibili molto più snelli, risparmiando così spazio
disco.
La creazione di una libreria condivisa è
molto simile alla creazione di una libreria statica. Si compila una lista
di oggetti e li si colleziona in un unico file. Ci sono però due
differenze importanti:
Dobbiamo compilare per "Position
Independent Code" (PIC).
Visto che al momento della creazione dei moduli oggetto non sappiamo in quale
posizione della memoria saranno inseriti nei programmi che li useranno, tutte
le chiamate alle funzioni devono usare indirizzi relativi e non assoluti.
Per generare questo tipo di codice si passa al compilatore la flag
"-fpic" o "-fPIC" nella fase di compilazione dei moduli
oggetto.
Contrariamente alle librerie statiche, quelle
dinamiche non sono file di archivio. Una libreria condivisa ha un
formato specifico che dipende dall'architettura per la quale è stata
creata. Per generarla di usa o il compilatore stesso con la flag
"-shared" o il suo linker.
Consideriamo ancora una volta il nostro programma
di prova. I comandi per la creazione di una libreria condivisa possono
presentarsi come segue:
Nel scegliere il nome di una libreria condivisa è stata
utilizzata la convenzione secondo cui il nome del file della libreria inizia
con il prefisso ''lib'' e termina con il suffisso
".so''.
I primi due comandi compilano i moduli oggetto con l'opzione
(fPIC) in maniera tale che essi siano utilizzabili per una libreria condivisa
(possiamo comunque utilizzarli in un programma normale anche se sono stati
compilati con PIC). L'ultimo comando chiede al compilatore di generare la
libreria dinamica.
./prova -10 3
-3.33333
**Severe Error in "double log(double)":Invalid argument.
Quitting now.
Come costruire una libreria statica
Per costruire una libreria statica bisogna
partire dai moduli oggetto dei nostri sorgenti.
g++ -c myfuncs.cpp errmsg.gcc
ar r libmath_util.a myfuncs.o errmsg.o
ar tv libmath_util.a
rw-r--r-- 223/100 18256 Dec 10 14:24 2003 errmsg.o
rw-r--r-- 223/100 23476 Dec 10 14:23 2003 myfuncs.o
Link con una libreria statica
g++ -o prova_s main.cpp -L. -lmath_util
I limiti del meccanismo del link statico
Librerie condivise
Come costruire una libreria condivisa
g++ -fPIC -c myfuncs.cpp
g++ -fPIC -c errmsg.cpp
g++ -shared -o libmath_util.so myfuncs.o errmsg.o
Come abbiamo già preannunciato l'uso di una libreria condivisa si articola in due momenti: Compile time e Run Time. La parte di compilazione e semplice. Il link ad una libreria condivisa avviene in maniera del tutto simile al caso di una libreria statica
g++ -o prova_d main.cpp -L. -lmath_utilDove abbiamo chiamato l'eseguibile prova_d per ricordarci che è stato ottenuto tramite il link alla libreria dinamica.
Se però proviamo a lanciare l'eseguibile otteniamo una sgradita sorpresa:
./prova_d -10 3 ./prova_d: error while loading shared libraries: libmath_util.so:cannot open shared object file: No such file or directoryIl dynamic loader non è in grado di localizzare la nostra libreria!
Possiamo infatti usare il comando ldd per verificare le dipendenze delle librerie condivise e scoprire che la nostra libreria non viene localizzata dal loader dinamico:
ldd ./prova_dlibmath_util.so => not found libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0x40030000) libm.so.6 => /lib/tls/libm.so.6 (0x400e3000) libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x40106000) libc.so.6 => /lib/tls/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)Ciò avviene perchè la nostra libreria non risiede in una directory standard.
Ci sono diversi modi per specificare la posizione delle librerie condivise nell'ambiente linux. Se avete i privilegi di root, una possibilità è quella di aggiungere il path della nostra libreria al file /etc/ld.so.conf per poi lanciare /sbin/ldconfig . Ma se non avete l'accesso all'utente root, potete sfruttare la variabile ambiente LD_LIBRARY_PATH per dire al dynamic loader dove cercare la nostra libreria:
setenv LD_LIBRARY_PATH /home/murgia/C++/ldd ./prova_dlibmath_util.so => /home/murgia/C++/libmath_util.so (0x40017000) libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0x40030000) libm.so.6 => /lib/tls/libm.so.6 (0x400e3000) libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x40106000) libc.so.6 => /lib/tls/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)In questo caso il programma ldd ci informa che ora il dynamic loader è in grado di localizzare libmath_util.so, ed il programma sarà eseguito con successo.
Esiste anche la possibilità di passare al linker la locazione della nostra librerie con l'opzione -rpath in questa maniera
g++ -o prova_d main.cpp -Wl,-rpath,/home/murgia/C++/ -L. -lmath_utilin questo caso non sarà necessario preoccuparsi di definire la variabile ambiente LD_LIBRARY_PATH.
Si faccia però attenzione al fatto che il linker da' la precedenza al path specificato con -rpath, se questo non è specificato allora usa il valore di LD_LIBRARY_PATH, e solo infine verifica il contenuto del file /etc/ld.so.conf.
Se nella stessa directory sono presenti sia libmath_util.so che libmath_util.a il linker preferirà la prima. Per forzare il linker ad utilizzare la libreria statica si può usare la flag -static.
Diversi programmi che fanno uso di librerie comuni possono essere corretti contemporaneamente intervenendo sulla libreria che è fonte di errore. La sola ricompilazione e sostituzione della libreria risolve un problema comune.
Per riassumere:
Librerie statiche:
Ogni processo ha la sua copia della libreria statica che sta usando, caricata in memoria.
Gli eseguibili collegati con librerie statiche sono più grandi.
Librerie condivise:
Solo una copia della libreria viene conservata in memoria ad un dato istante (sfruttiamo meno memoria per far girare i nostri programmi e gli eseguibili sono più snelli).
I programmi partono più lentamente.