Link Dinamico e Statico su Linux

Nella prima parte di questo articolo riguardante il linking nei sistemi Unix-like con gcc e file tipo ELF, abbiamo parlato del formato dei file oggetto e come crearli, questa volta vedremo la fase di link sia statico che dinamico.
Se analizziamo il file main.o con lo strumento objdump per osservare il codice assembler generato

objdump –disassemble –souce –reloc main.o

possiamo notare come  i simboli delle funzioni esterne abbiano ancora una locazione di memoria fittizia, sarà infatti il link editor a risolvere i simboli esterni a partire dalle due tabelle del file oggetto e generare l’eseguibile con i riferimenti esterni giusti. Questo tipo di linking è statico, perché la costruzione del programma avviene interamente a tempo di compilazione con i simboli e funzioni esterni inglobati totalmente nell’eseguibile e che quindi può funzionare anche senza l’ausilio di dette librerie esterne ( in Linux le librerie statiche solitamente hanno estensione .a es: libc.a ). Un approccio di questo tipo deve essere valutato attentamente, in quanto si tende a generare codice ridondante, che non solo aumenta a dismisura la grandezza del nostro sistema, ma soprattutto non consente aggiornamenti delle funzioni a meno di non modificare tutti i sorgenti dei nostri programmi.
Per ovviare al problema del link statico si utilizza il link dinamico e la creazione di librerie condivise dalle applicazioni, le DLL ( Dynamic Link Library ) ( in linux le librerie dinamiche solitamente hanno estenzione .so ed il numero di versione es: libc.so.6 ). Con l’uso delle dll, il collegamento dinamico avviene nella fase di loading del programma, quindi il processo di linking è ritardato fino a questo momento. In fase di compilazione di un eseguibile o di una libreria, il linker memorizza in apposite sezioni del file oggetto opportune informazioni necessarie per identificare i moduli software da agganciare in fase di caricamento. Le principali informazioni, per i file ELF contenute nella sezione .dynamic, sono:

  • il nome, ed opzionalmente il path, delle librerie da agganciare all’eseguibile
  • la versione di ogni singola libreria
  • l’elenco dei simboli da risolvere e rilocare per ogni libreria

Ad esempio, eseguendo il comando nm sul nostro file eseguibile somma avremo:

nm –undefined-only somma


printf@@GLIBC_2.0

indicando che la funzione printf è undefined e reperibile nella libreria indicata “GLIBC_2.0” che sono le librerie basilari per un sistema Linux. Se vediamo l’output del seguente comando:

objdump –all-headers somma

vedremo che nella sezione .Dynamic abbiamo come necessaria ( needed ) la libreria libc.so.6, da cui il nostro applicativo dipende. La speciale sezione .Dynamic è utilizzata da molti programmi tra cui il famoso ed utilissimo ldd che consente di vedere le dipendenze di una libreria o di un eseguibile. Provate a dare il comando

ldd /lib/libc.so.6

e vedrete come la libreria libc.so.6 dipenda dal solo ld-linux.so.2. Questo file è un interprete della GNU che è comune a tutti i sistemi Linux e che consente di caricare il programma. Il loader del sistema operativo, infatti,  non carica direttamente l’eseguibile, ma invoca un interprete, che si trova nella sezione .interp del file oggetto, che svolge la funzionalità di linker dinamico.
Il linker dinamico costruito dalla GNU è fornito con la libreria glibc e presenta interessanti funzionalità. Queste peculiarità sono attivabili attraverso la dichiarazione di speciali variabili d’ambiente. Ad esempio, molto utile per fini amministrativi è quella di debug, infatti settando la variabile d’ambiente LD_DEBUG.

LD_DEBUG=files
export LD_DEBUG

Ogni volta che lanciamo un programma, vedremo sull’stdout tutte le operazioni svolte per processare l’applicazione. Potete tranquillamente fare questo esperimento in una finestra terminale, perché la variabile LD_DEBUG sarà valida solo in quella shell. Oltre a files possiamo utilizzare anche versions che mostra l’effettiva versione delle librerie che vengono agganciate al programma o statistics per mostrare i tempi di linking. Quest’ultima opzione può essere utile per capire il variare dei tempi di linking modificando l’ordine di ricerca delle librerie nel file /etc/ld.so.conf e quindi ottimizzando l’intero sistema. Per la lista delle possibili funzionalità basta settare la variabile LD_DEBUG con il parametro help. Per avere tutte le informazioni settarla con all. Un’altra interessante caratteristica è la variabile d’ambiente LD_PRELOAD che consente di caricare una specifica libreria prima delle altre, ad esempio per invocare una funzione costruita ad hoc da noi stessi, senza avere i sorgenti di un programma o libreria di terzi. Facciamo il caso della funzione somma che risieda su un’ipotetica nostra libreria myLib

LD_PRELOAD=…/myLib.so somma
export LD_PRELOAD

Un’altra variabile d’ambiente importante è LD_LIBRARY_PATH per settare il percorso di ricerca senza modificare il file /etc/ld.so.conf, se ad esempio non abbiamo i privilegi per potervi accedere. Per maggiori informazioni rimando all’aiuto in linea delle librerie glibc della GNU.
Nella terza ed ultima parte di questo articolo affronteremo l’argomento degli shared object e del problema della memoria. Ricordo ancora che per qualsiasi domanda o chiarimento potete iscrivervi al forum su Linux.

Di Giampaolo Rossi

Sviluppatore software da oltre 16 anni.