Librerie statiche e dinamiche in Linux


Introduzione

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. 


Librerie in ambiente Linux

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:

Ognuna presenta vantaggi e svantaggi, ma tutte hanno una cosa in comune: costituiscono un catalogo di funzioni, classi, etc..., che ogni programmatore può riutilizzare.


Un programma di prova

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.cpp

L'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:

./prova -10 3

-3.33333

**Severe Error in "double log(double)":Invalid argument.
Quitting now.


Librerie statiche

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. 


Come costruire una libreria statica

Per costruire una libreria statica bisogna partire dai moduli oggetto dei nostri sorgenti.
g++ -c myfuncs.cpp errmsg.gcc

Una volta compilati i moduli myfuncs.o e errmsg.o, costruiamo la libreria statica libmath_util.a con il programma di archiviazione ar:

 

ar r libmath_util.a myfuncs.o errmsg.o

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

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

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:

g++ -o prova_s main.cpp -L. -lmath_util

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.


I limiti del meccanismo del link statico

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.


Librerie condivise

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.


Come costruire una libreria condivisa

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:

  1. 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. 

  2. 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: 

g++ -fPIC -c myfuncs.cpp

g++ -fPIC -c errmsg.cpp

g++ -shared -o libmath_util.so myfuncs.o errmsg.o

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. 


Link con una libreria condivisa

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_util

Dove 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 directory

Il 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_d
libmath_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. 


La variabile ambiente LD_LIBRARY_PATH

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_d
libmath_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.


La flag -rpath

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_util

in 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.  


Che tipo di libreria sto usando?

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.


Un aspetto positivo dell'utilizzo delle librerie condivise

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.


Librerie statiche vs librerie condivise

Per riassumere:

Librerie statiche:

 

Librerie condivise:

 


 

Torna all'Indice