Avanti Indietro Indice

6. Linking

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.

6.1 Librerie condivise e statiche

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)'.

6.2 Interrogazione delle librerie ('In quale libreria si trova sin()?')

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.

6.3 Ricerca dei file

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.

6.4 Compilazione delle proprie librerie

Controllo della versione

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. Di cosa si tratta?

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.

Uteriori dettagli

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 chiamata elf_open() si eseguono chiamate elf_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.

Librerie condivise di ELF

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 )

Numerazione delle versioni, soname e symlink

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.

a.out. Il formato tradizionale

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.

ZMAGIC e QMAGIC

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.

Posizione dei file

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.

"libc-lite"?

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.

Linking: problemi comuni

Inviatemi i problemi derivanti dal linking! Anche se non potrò fare niente per risolverli, ne scriverò un resoconto dettagliato.

Programmi eseguono il link statico anziché dinamico

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.

Lo strumento DLL 'mkimage' non riesce a trovare libgcc

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.

Messaggi __NEEDS_SHRLIB_libc_4 multiply defined

Altra conseguenza del problema descritto al punto precedente.

Messaggio ``Assertion failure'' quando si ricompila una DLL?

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.

6.5 Loading dinamico

Questo paragrafo è per il momento piuttosto breve, verrà esteso in futuro.

Concetti

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

Messaggi di errore

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.

Controllo delle operazioni del loader dinamico

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

Scrivere programmi con il loading dinamico

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");
    }

}


Avanti Indietro Indice