Questo paragrafo è piuttosto complicato a causa dei due formati binari incompatibili, la distinzione tra libreria statica e condivisa, e del sovraccarico del verbo 'link' che significa sia 'cosa accade dopo la compilazione', sia 'cosa accade quando viene richiamato un programma compilato' (e, di fatto, il sovraccarico del verbo 'load' in un senso simile ma opposto).
Per ridurre in qualche modo la confusione, si userà il termine `caricamento dinamico' (dynamic loading) per quello che accade durante l'esecuzione, argomento descritto nel prossimo paragrafo. Potrebbe anche accadere di trovare il termine 'collegamento dinamico' (dynamic linking) con lo stesso significato, ma non in questo documento. Questo paragrafo, quindi, tratta esclusivamente il tipo di link che avviene alla fine di una compilazione.
L'ultima fase della compilazione di un programma consiste
nel 'collegamento' (link), ossia nell'unire tutte le parti e vedere se
manca qualcosa. Ovviamente esistono alcune cose che molti
programmi hanno in comune - ad esempio, aprire file, ed il
codice in grado di fare queste cose sono fornite sotto forma di librerie.
Nella maggior parte dei sistemi Linux si trovano in /lib
e
/usr/lib/
.
Quando si utilizza una libreria statica, il linker trova
le parti necessarie ai moduli di programma e le copia
fisicamente nel file di output eseguibile che viene
generato. Al contrario, questo non avviene per le librerie condivise - in
questo caso nell'output viene inserita una nota del tipo 'quando
viene eseguito questo programma, è necessario caricare questa libreria'.
Ovviamente, le librerie condivise tendono a creare eseguibili di
dimensioni minori; inoltre utilizzano una quantità inferiore di memoria e
viene utilizzato meno spazio su disco. Il comportamento predefinito di
Linux consiste nell'eseguire il collegamento di librerie condivise se
esistono, altrimenti vengono utilizzate quelle statiche. Se si ottengono
dei binari statici quando, al contrario, si vogliono quelli condivisi,
controllare che i file di libreria condivisa (*.sa
per a.out,
*.so
per ELF) si trovino dove dovrebbero essere e siano leggibili.
Su Linux, le librerie statiche hanno nomi come libname.a
, mentre
le librerie condivise sono denominate libname.so.x.y.z
dove
x.y.z
rappresenta il numero di versione. Le librerie
condivise, inoltre, contengono spesso dei collegamenti che puntano ad
esse, che risultano essere molto importanti, e (in configurazioni a.out)
contengono dei file .sa
associati. Le librerie standard
sono disponibili sia in formato condiviso, sia in formato statico.
È possibile sapere quali librerie condivise sono richieste da un programma
utilizzando ldd
(List Dynamic Dependencies)
$ ldd /usr/bin/lynx
libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
libc.so.5 => /lib/libc.so.5.2.18
Questo esempio mostra che il browser WWW 'lynx' dipende dalla
presenza di libc.so.5
(la libreria C) e libncurses.so.1
(utilizzata per il controllo del terminale). Se un programma non ha
dipendenze, ldd
risponderà 'statically linked'
o 'statically linked (ELF)'.
nm
nome_libreria dovrebbe listare tutti i simboli per i
quali esistono dei riferimenti in nome_libreria. Il comando funziona
sia per librerie statiche che dinamiche. Si supponga di voler saper dov'è
definita tcgetattr()
: si potrebbe utilizzare
$ nm libncurses.so.1 |grep tcget
U tcgetattr
`U
' significa 'undefined' - mostra che la libreria ncurses
la utilizza ma non la definisce. In alternativa, si potrebbe usare
$ nm libc.so.5 | grep tcget
00010fe8 T __tcgetattr
00010fe8 W tcgetattr
00068718 T tcgetpgrp
`W
' significa 'weak', ossia che il simbolo è definito, ma
in modo tale da poter essere sostituito da un'altra definizione in una
libreria diversa. Una definizione 'normale' (come quella per
tcgetpgrp
) è indicata con una 'T
'.
Comunque la risposta breve alla domanda del titolo, consiste in
libm.(so|a)
. Tutte le funzioni definite in <math.h>
sono tenute nella libreria math
; ne consegue che sarà
necessario eseguire il collegamento con l'opzione -lm
quando si
utilizza una di esse.
ld: Output file requires shared library `libfoo.so.1`
(Ovvero: "ld: Il file di output richiede la libreria
condivisa 'libfoo.so.1'")
La strategia di ricerca di un file per ld e simili varia a
seconda della versione, ma l'unico punto che si può ritenere
predefinito è /usr/lib
. Se si vuole che le librerie vengano
cercate in altre locazioni, è necessario specificare le loro directory
tramite l'opzione -L
in gcc o ld.
Se non dovesse funzionare, controllare che i file necessari
si trovino effettivamente in quelle directory. Per a.out, il
collegamento con -lfoo
fa in modo che ld cerchi libfoo.sa
(condivisa) e, in caso di insuccesso, libfoo.a
(statica). Per ELF,
verrà eseguita la ricerca di libfoo.so
, quindi di libfoo.a
.
libfoo.so
è generalmente un collegamento simbolico a
libfoo.so.x
.
Come qualunque altro programma, le librerie possono contenere errori che vengono riscontrati e risolti nel tempo. Inoltre, le librerie possono introdurre nuove caratteristiche, modificare l'effetto di altre esistenti, o rimuovere quelle vecchie. Questo potrebbe costituire un problema per i programmi che le utilizzano.
Pertanto si è introdutto il concetto di versione della libreria. Tutte le
modifiche che possono essere fatte a una libreria sono catalogate
in 'minori' o 'maggiori', dove una modifica 'minore' non interrompe il
funzionamento dei vecchi
programmi che la utilizzano. La versione di una libreria può essere
dedotta dal suo nome di file (di fatto, questo non è vero per quanto
riguarda ELF; nel seguito viene spiegato il motivo): libfoo.so.1.2
ha '1' come versione maggiore, e '2' come minore. Il numero di
versione minore può essere svariato - libc inserisce in esso il 'livello
di patch', assegnando alle librerie nomi del tipo libc.so.5.2.18
, e
spesso sono utilizzate lettere, underscore, o quasi ogni altro carattere
ASCII.
Una delle differenze principali tra il formato ELF e a.out consiste nel modo in cui viene eseguita la compilazione delle librerie condivise. Per prima cosa viene descritto ELF, dal momento che è più semplice.
ELF (Executable and Linking Format) è un formato binario originariamente sviluppato da USL (UNIX System Laboratories) e attualmente utilizzato in Solaris e System V Release 4. A seguito della sua aumentata flessibilità rispetto al più vecchio formato a.out utilizzato da Linux, gli sviluppatori di librerie GCC e C hanno deciso lo scorso anno di utilizzare ELF come formato binario standard per Linux.
Questo paragrafo è tratto dal documento '/news-archives/comp.sys.sun.misc
'.
ELF (Executable Linking Format) è il nuovo e migliorato formato di file oggetto introdotto in SVR4. ELF è molto più potente di COFF, nel fatto di essere estendibile dall'utente. ELF vede un file oggetto come una lista di sezioni arbitrariamente lunga (piuttosto che come un array di entità a lunghezza fissa), tali sezioni, a differenza di quanto accade in COFF, non si devono trovare in un luogo specifico e non devono essere in un ordine specifico ecc. Gli utenti possono aggiungere nuove sezioni ai file oggetto, se desiderano avere a disposizione nuovi dati. ELF, inoltre, possiede un formato di debugging molto più potente denominato DWARF (Debugging With Attribute Record Format) - attualmente non supportato completamente su Linux (ma ci si sta lavorando). Una lista linkata di DIE (o Debugging Information Entries) di DWARF costituisce la sezione.debug
di ELF. Invece di essere un insieme di piccoli record a dimensione fissa, ogni DIE di DWARF contiene una lista di lunghezza arbitraria di attributi complessi ed è scritto come un albero di dati di programma. I DIE sono in grado di includere una quantità di informazioni di molto maggiore rispetto alla sezione.debug
di COFF (come grafi di eredità del C++ ecc).
L'accesso ai file ELF avviene tramite la libreria di accesso ELF SVR4 (Solaris 2.0 ?), che fornisce un'interfaccia semplice e rapida alle parti più complicate di ELF. Uno dei vantaggi principali derivanti dall'utilizzo della libreria di accesso ELF consiste nel fatto che non sarà mai necessario vedere un file ELF come file Unix, è possibile eseguire l'accesso come file Elf *, dopo una chiamataelf_open()
si eseguono chiamateelf_foobar()
sulle sue componenti invece di occuparsi della sua immagine effettiva su disco (cosa che con COFF si faceva impunemente).
I vantaggi e gli svantaggi di ELF, e le evoluzioni necessarie per eseguire l'upgrade di un sistema a.out per supportarlo, sono descritti nell'ELF-HOWTO e non ho intenzione di ripeterli qui. L'HOWTO dovrebbe essere disponibile nello stesso luogo in cui è stato trovato questo documento.
Per eseguire la compilazione di libfoo.so
come libreria
condivisa, i passi di base hanno la seguente forma:
$ gcc -fPIC -c *.c
$ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o
$ ln -s libfoo.so.1.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so
$ LD_LIBRARY_PATH='pwd':$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH
Questi comandi genererano una libreria condivisa denominata
libfoo.so.1.0
, i collegamenti appropriati per ld (libfoo.so
) e
il caricamento dinamico (libfoo.so.1
) per trovarla. Per eseguire un
collaudo, si aggiunge la directory corrente a LD_LIBRARY_PATH
.
Quando si è sicuri che la libreria funziona, deve essere spostata,
ad esempio, in /usr/local/lib
, e devono essere creati appropriati
collegamenti. Il collegamento da libfoo.so.1
a libfoo.so.1.0
è mantenuto aggiornato da ldconfig
, che nella maggior parte dei
sistemi viene eseguito come parte del processo di avviamento. Il
collegamento libfoo.so
deve essere aggiornato manualmente.
Se si è scrupolosi nell'eseguire l'aggiornamento di tutte le parti di una
libreria (ossia degli header file) contemporaneamente, la cosa più
semplice da fare consiste nel rendere libfoo.so -> libfoo.so.1
,
in modo che ldconfig mantenga correnti entrambi i collegamenti. In caso
contrario, potrebbe in seguito verificarsi ogni genere di stranezza.
$ su
# cp libfoo.so.1.0 /usr/local/lib
# /sbin/ldconfig
# ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so )
Ogni libreria ha un soname. Quando il linker trova uno di
questi in una libreria in cui sta eseguendo una ricerca, nel binario viene
incluso il soname in luogo del nome di file effettivo ricercato. Durante
l'esecuzione, il loader dinamico cercherà un file tramite il nome di
soname, non con il nome di file/libreria. Pertanto, una libreria
denominata libfoo.so
potrebbe avere il soname libbar.so
, di
conseguenza tutti i programmi collegati ad essa, all'avvio, cercherebbero
libbar.so
.
Sembra essere una caratteristica di poca importanza, invece è la chiave per
capire come su un sistema possono coesistere diverse versioni della stessa
libreria. Di fatto, la denominazione standard delle librerie in Linux
consiste nel chiamare la libreria, ad esempio, libfoo.so.1.2
, e
assegnare ad essa il soname libfoo.so.1
. Se aggiunta in una
directory di libreria 'standard' (ossia, /usr/lib
),
ldconfig creerà un collegamento simbolico
libfoo.so.1 -> libfoo.so.1.2
in modo che sia possibile trovare
l'immagine appropriata durante l'esecuzione. È necessario anche un
collegamento libfoo.so -> libfoo.so.1
affinché ld possa trovare
il soname corretto da utilizzare durante il link.
Pertanto, quando si risolve un errore nella libreria, o si aggiungono
nuove funzioni (ogni modifica che non influenzi in modo negativo i
programmi esistenti), si eseguirà nuovamente la compilazione mantenendo lo
stesso soname, e modificando il nome di file. Quando si inseriscono
nella libreria delle modifiche che causerebbero l'interruzione dei
programmi esistenti, incrementare semplicemente il numero nel soname - in
questo caso, rinominare la nuova versione libfoo.so.2.0
, e assegnarle
il soname libfoo.so.2
. Quindi, convertire il collegamento a
libfoo.so
in modo che punti alla nuova versione e tutto è a posto.
Si noti che non è necessario dare un nome alle librerie, ma si tratta di una buona convenzione. ELF fornisce una flessibilità nel nominare le librerie in modi che potrebbero confondere, ma questo non significa che debba farlo per forza.
Riassumendo: se si suppone di osservare la tradizione secondo cui gli aggiornamenti maggiori potrebbero distruggere la compatibilità e che quelli minori non lo fanno, eseguire il collegamento con
gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor
e tutto funzionerà alla perfezione.
La facilità con cui si esegue la compilazione di librerie condivise è uno dei motivi principali per passare a ELF. Detto questo, è ancora possibile utilizzare a.out. Si prenda ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz e si legga il documento di 20 pagine.
QMAGIC è un formato eseguibile proprio come i vecchi binari a.out (conosciuti anche come ZMAGIC), ma che lascia la prima pagina non mappata. Questo consente che accada più facilmente un riferimento NULL dal momento che non esiste alcun mapping nel range 0-4096. Come effetto collaterale, i binari saranno di dimensioni inferiori (di circa 1 K).
I linker obsoleti supportano solamente ZMAGIC, quelli semi-obsoleti supportano entrambi i formati, mentre le versioni attuali supportano solo QMAGIC. In realtà questo non ha importanza, dal momento che il kernel riesce ad eseguire entrambi i formati.
Il proprio comando 'file' dovrebbe essere in grado di identificare se un programma è QMAGIC.
Una libreria condivisa a.out (DLL) è formata da due file reali e da
un collegamento simbolico. Per la libreria 'foo
' utilizzata come
esempio, questi file sarebbero libfoo.sa
e libfoo.so.1.2
;
il collegamento simbolico sarebbe libfoo.so.1
e punterebbe all'ultimo
di questi file. Ma a cosa servono?
Durante la compilazione, ld ricerca libfoo.sa
. Si tratta del
file 'matrice' della libreria, e contiene tutti i dati esportati e i
puntatori alle funzioni richieste per il collegamento run time.
Durante l'esecuzione, il loader dinamico cerca libfoo.so.1
. Si
tratta di un collegamento simbolico anziché di un file reale in
modo che le librerie possano essere aggiornate con versioni più recenti e
corrette senza procurare danni a nessuna delle applicazioni utilizzanti la
libreria in quel momento. Dopo che la nuova versione, ad esempio
libfoo.so.1.3
- è stata introdotta, l'esecuzione di ldconfig
commuterà il collegamento affinché punti ad essa tramite un'operazione
atomica, lasciando illeso ogni programma che stava utilizzando la vecchia
versione.
Le librerie DLL appaiono spesso più grandi delle loro controparti
statiche. Riservano spazio per future espansioni nella forma di 'buchi'
che possono essere creati senza occupare spazio su disco. Una semplice
chiamata cp
o l'utilizzo del programma makehole
raggiungerà
questo scopo. Dopo la compilazione, è anche possibile rimuoverli, dal
momento che gli indirizzi si trovano in locazioni fisse. Non tentare di
rimuoverli dalle librerie ELF.
Un libc-lite è una versione ridotta della libreria libc per la quale è
stata eseguita la compilazione in modo tale da stare su un floppy
disk ed essere sufficiente per tutti i task Unix essenziali. Non
include codice curse
, dbm
, termcap
ecc. Se il proprio
/lib/libc.so.4
ha un collegamento con un lite lib, il
sistema avvisa di sostituirlo con una versione completa.
Inviatemi i problemi derivanti dal linking! Anche se non potrò fare niente per risolverli, ne scriverò un resoconto dettagliato.
Controllare di avere i collegamenti corretti affinché ld possa trovare
tutte le librerie condivise. Per ELF questo significa un collegamento
simbolico libfoo.so
per l'immagine, in a.out un file libfoo.sa
.
Molte persone hanno riscontrato questo problema dopo il passaggio dai
binutils ELF 2.5 ai 2.6 - la versione precedente ricercava le librerie
condivise in un modo 'più intelligente' pertanto non avevano bisogno di
creare tutti i collegamenti. Il comportamento intelligente è stato
eliminato per compatibilità con altre architetture, e perché molto spesso
le sue supposizioni erano sbagliate e causando più guai di quanti fossero
i problemi risolti.
Da libc.so.4.5.x
e oltre, libgcc
non è più condivisa. Pertanto,
è necessario sostituire le occorrenze di '-lgcc
'
con 'gcc -print-libgcc-file-name
'.
Inoltre, bisogna cancellare tutti i file /usr/lib/libgcc*
. Questo
è molto importante.
__NEEDS_SHRLIB_libc_4 multiply defined
Altra conseguenza del problema descritto al punto precedente.
Questo messaggio criptico molto probabilmente significa che uno degli
slot della propria jump table è andato in overflow poiché è
stato riservato troppo poco spazio nel file jump.vars originale. È
possibile localizzare i colpevoli eseguendo il
comando 'getsize
' fornito nel pacchetto tools-2.17.tar.gz
. Tuttavia,
probabilmente l'unica soluzione consiste nel sostituire il numero di
versione maggiore della libreria, forzandolo affinché sia impossibile
tornare indietro.
ld: output file needs shared library libc.so.4
Questo accade solitamente quando si sta eseguendo il collegamento con
librerie diverse da libc (come le librerie X), e si utilizza l'opzione
-g
sulla riga di link utilizzando anche -static
.
Gli stub .sa
per le librerie condivise contengono solitamente
un simbolo indefinito _NEEDS_SHRLIB_libc_4
che viene risolto da
libc.sa
. Tuttavia, con -g
si finisce di eseguire il
collegamento con libg.a
o libc.a
, il simbolo non
viene mai risolto, portando all'errore sopra descritto.
In conclusione, aggiungere -static
quando si compila con
l'opzione -g
, oppure non eseguire il collegamento con
-g
. Molto spesso è possibile ottenere informazioni di
debugging sufficienti compilando i file individuali con
-g
, ed eseguendo il collegamento senza questa opzione.
Questo paragrafo è per il momento piuttosto breve, verrà esteso in futuro.
Linux possiede delle librerie condivise, come si è visto diverse molte volte nell'ultimo paragrafo. Gran parte del lavoro di associazione dei nomi a locazioni, che tradizionalmente era svolto al momento del link, deve essere ritardato al momento del load (caricamento).
I lettori sono pregati di inviare i proprii errori di link all'autore, che anche se non potrà risolverli, comunque scriverà un resoconto dettagliato.
can't load library: /lib/libxxx.so, Incompatible version
(solo in a.out) Questo significa che non si possiede la versione maggiore aggiornata della libreria xxx. Non è possibile semplicemente creare un collegamento simbolico ad un'altra versione che si possiede; nella migliore delle ipotesi questo causerà un segfault nel proprio programma. Si consiglia di ottenere una nuova versione. Una situazione simile in ELF produrrà un messaggio del tipo
ftp: can't load library 'libreadline.so.2'
warning using incompatible library version xxx
(solo in a.out) Si possiede una versione minore della libreria più vecchia di quella posseduta dalla persona che ha compilato il programma in questione. Il programma funzionerà comunque. Tuttavia, un aggiornamento non sarebbe una cattiva idea.
Esistono diverse variabili di ambiente a che influenzano il comportamento del
loader dinamico. La maggior parte di esse sono più utili a ldd
di quanto non lo siano per l'utente medio, e possono essere impostate
eseguendo ldd con diverse opzioni. Includono
LD_BIND_NOW
--- normalmente, le funzioni non sono ricercate
nelle librerie finché non vengono chiamate. L'impostazione
di questa opzione attiva la ricerca all'atto del caricamento della libreria,
determinando un tempo di avviamento maggiore. Può essere
utile quando si vuole collaudare un programma per accertarsi che sia eseguito
il link di tutte le parti.
LD_PRELOAD
--- può essere impostato con un file contenente delle
definizioni di funzioni da sovrapporre. Ad esempio, se si sta eseguendo un
test delle strategie di allocazione della memoria, e si vuole
sostituire 'malloc', è possibile scrivere la propria routine
sostitutiva, compilarla come malloc.o
e utilizzare i comandi
$ LD_PRELOAD=malloc.o; export LD_PRELOAD
$ programma_di_test
LD_ELF_PRELOAD
e LD_AOUT_PRELOAD
sono simili, ma possono essere
applicati solo al tipo binario appropriato. Se
LD_
qualcosa_PRELOAD
e LD_PRELOAD
sono entrambi impostati,
verrà utilizzato quello più specifico.
LD_LIBRARY_PATH
--- elenco, separato da virgole, di
directory in cui ricercare le librerie condivise. Non ha effetti
su ld, ma solo durante l'esecuzione. Inoltre, è disabilitato per programmi che
eseguono setuid o setgid. LD_ELF_LIBRARY_PATH
e LD_AOUT_LIBRARY_PATH
possono anche essere utilizzati per impostare la
ricerca in modo differente per diversi tipi di binari. LD_LIBRARY_PATH
non dovrebbe essere necessario nelle operazioni normali; piuttosto aggiungere
le directory a /etc/ld.so.conf/
ed eseguire ldconfig.
LD_NOWARN
--- si applica solo ad a.out. Quando impostato (ossia
con LD_NOWARN=true; export LD_NOWARN
) evita che il loader fornisca i
warning non fatali (come i messaggi per incompatibilità di versione
minore).
LD_WARN
--- si applica solamente a ELF. Quando impostato, rende
i messaggi, solitamente fatali, "Can't find library" dei semplici warning.
Non è molto utilizzato nelle operazioni normali, ma è importante per ldd.
LD_TRACE_LOADED_OBJECTS
--- si applica solamente a ELF, e fa in
modo che i programmi credano di essere in esecuzione sotto ldd:
$ LD_TRACE_LOADED_OBJECTS=true /usr/bin/lynx
libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
libc.so.5 => /lib/libc.so.5.2.18
Questo è molto simile al funzionamento del supporto di
loading dinamico di Solaris 2.x.
L'argomento è trattato ampiamente nel documento di
programmazione ELF di H J Lu e nella pagina di manuale
dlopen(3) manual page
, che può essere trovata nel pacchetto
ld.so. Segue un semplice esempio: è necessario effettuare il link con
-ldl
#include <dlfcn.h>
#include <stdio.h>
main()
{
void *libc;
void (*printf_call)();
if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY))
{
printf_call=dlsym(libc,"printf");
(*printf_call)("hello, world\n");
}
}