Static e dynamic linking: static && shared libraries - STB1019/SkullOfSummer GitHub Wiki

Introduzione

L'ingegneria del software ha, come uno dei suoi principi, il riuso del codice come modo per velocizzare e standardizzare il prodotto software. Grazie al riuso di codice, infatti, è possibile evitare di reinventare la ruota e sfruttare ciò che già altri hanno progettato ed implementato. Dato un programma risulta spesso comodo riusare del codice ed è quindi importante referenziare codice preesistente: una soluzione sarebbe quella di copiare ed incollare i sorgenti "header" e "c" direttamente nel progetto in corso ma ciò creerebbe una copia da dover gestire esplicitamente nel proprio codice. Una soluzione molto più sensata è quella di creare delle librerie.

Librerie

Le librerie sono collegate ad un programma in fase di compilazione dal linker. Quando si vuole usare una libreria in uno dei propri programmi è necessario conoscere 2 flag del gcc:

  1. -L ti permette di definire dove andare a cercare le librerie che ti interessano. Oltre alle directory specificate da -L, il gcc cerca anche in altre cartelle di sistema (per esempio /usr/lib). Tuttavia, se tu hai posizionato una libreria in una cartella del file system non convenzionale, questa flag ti permette di notificarlo al gcc;

  2. -l ti permette di definire qual'è la libreria che ti interessa utilizzare. Questo flag è opportuno metterlo sempre alla fine del comando gcc (l'ordine dei comandi impartiti al gcc ha importanza) e senza spazi.

Il codice degli esempi proposti in questa pagina è reperibile qui.

Librerie statiche

Le librerie statiche sono collezioni di file "*.o" (codice macchina) aggregati in un unico file avente come estensione "*.a". Le librerie statiche sono linkate in un programma eseguibile dal linker e il loro codice viene inserito all'interno dell'eseguibile stesso. Ne consegue che i programmi linkati a librerie statiche saranno più pesanti in termini di dimensione. Una problematica degli eseguibili linkati a librerie statiche è che non c'è condivisione di codice: se hai 100 programmi che usano la stessa libreria statica, il codice della libreria statica sarà ripetuto 100 volte in memoria (quindi con un consumo di spazio). Il vantaggio della libreria statica è che il codice della libreria sarà direttamente reperibile all'interno dell'eseguibile: l'accesso ad una funzione della libreria avrà lo stesso costo, in termini di tempo, di una qualunque funzione definita all'interno del programma stesso.

Come si costruiscono?

Creare una libreria statica è semplice: Compila tutti i file oggetto ("*.o") normalmente, quindi chiama il comando ar per costruire la libreria statica. Per esempio:

gcc -g -c mathutils.c -o mathutils.o
gcc -g -c randutils.c -o randutils.o
ar -rc libawesomestatic.a mathutils.o randutils.o

Ricorda che all'interno della libreria sarà presente soltanto il codice relativo della libreria, ma nessun header: gli header devono essere manualmente salvati all'interno di una cartella di sistema (per esempio /usr/local/include)

Come si usano?

Per usare una libreria è necessario eseguire i seguenti comandi:

gcc -g -c useawesome.c -o userawesome.o #costruzione del nostro eseguibile
gcc -g useawesome.o -L. -lawesomestatic -o useawesomeStatic #linking alla libreria statica

dove al flag -l abbiamo posizionato il LINKER NAME della libreria (vedi "Nomenclatura delle shared library" per ulteriori informazioni).

Librerie dinamiche

Le librerie dinamiche sono anch'esse collezioni di file "*.o" aggregate in un unico file con estensione "*.so". Le librerie dinamiche sono anch'esse associate ad un codice in compilazione durante la fase di linking ma, a differenza delle libreria statiche, il codice della libreria non viene immediatamente inserito all'interno dell'eseguibile; spiegando meglio: per il compilatore è necessario in fase di compilazione, sapere che tutte le funzioni/variabili non definite nel programma sono definite in almeno una libreria dinamica definita (altrimenti il programma non compilerà) tuttavia il codice della libreria non viene affatto inserito nell'eseguibile. Quando l'utente lancerà il programma (ossia in fase di runtime), il sistema operativo andrà a leggere nell'eseguibile la lista di librerie dinamiche che il programma necessita per funzionare, andrà a cercarle in specifici luoghi del file system, associerà il codice macchina dell'eseguibile con il codice macchina della libreria e quindi farà partire l'eseguibile vero e proprio.

Da questa brevissima spiegazione delle librerie dinamiche si possono dedurre alcuni fatti: primo fra tutti l'eseguibile sarà di dimensioni ridotte, siccome il codice necessario non è totalmente presente nell'eseguibile. Secondariamente c'è la possibilità che il codice macchina della libreria possa essere condiviso tra più applicativi, risparmiando così memoria in runtime. Terzo fatto è che probabilmente ci sarà un rallentamento dato che il sistema operativo deve in un qualche modo collegare l'eseguibile con le shared library.

Per quanto concerne il processo di associazione eseguibile-libreria dinamica, esso è possibile in 2 varietà:

  • Load Time Relocation;
  • Position Independent Code;

La differenza è sottile è disponibile nei link Load Time Relocation e Position Independent Code.

Nomenclatura delle shared library

Prima di andare a capire come si costruiscono e si usano le shared library, dobbiamo necessariamente vedere le convenzioni sui nomi delle shared library.

Ogni shared library ha:

  • un SONAME, ossia una stringa che soddisfa il pattern "lib[NAME][API-VERSION].so.[major-version]" (dove API-VERSION è solitamente vuoto);
  • un REALNAME ossia una stringa che soddisfa il pattern "SONAME.[MINOR-NUMBER].[RELEASE-NUMBER]" ed è solitamente il nome del file che concretamente è presente nel file system. Estendendo SONAME, il pattern di REALNAME è "lib[NAME][API-VERSION].so.[MAJOR-VERSION].[MINOR-VERSION].[RELEASE-NUMBER]";
  • un LINKER NAME, ossia una stringa che da usare sul linker che identifica la libreria stessa.

Per intenderci, la nostra libreria dinamica reperibile qui avrà:

  • come "SONAME" "libawesomeshared.so"
  • come "REALNAME" "libawesomeshared.so"
  • come "LINKER NAME" "awesomeshared"

Come si costruiscono?

Per costruire una shared library bisogna leggermente modificare il processo di compilazione. In particolare bisogna aggiungere il flag -fPIC ad ogni compilazione di file oggetto da inserire nella libreria:

gcc -g -fPIC -c mathutils.c -o mathutils.o
gcc -g -fPIC -c randutils.c -o randutils.o

In seguito bisogna creare la libreria vera e propria tramite il comando:

#costruzione della libreria
gcc -g -shared -Wl,-soname,libawesomeshared.so mathutils.o randutils.o -o libawesomeshared.so

Mi raccomando nella sottostringa "-Wl,-soname,libawesomeshared.so" non ci vanno spazi aggiuntivi!

Come si usano?

Per usare una libreria dinamica bisogna eseguire dei passi simili rispetto a quelli fatti con la libreria statica. In particolare:

gcc -g useawesome.c -L. -lawesomeshared -o useawesomeShared

dove al flag -l abbiamo posizionato il LINKER NAME della libreria. Ricorda che tra -l e il nome della libreria non ci vanno spazi.

Dove posizionare header e librerie nel file system

Fino ad ora abbiamo parlato di dove sono posizionate le funzioni nella libreria. Nell'esempio proposto qui il programma eseguibile sa che la libreria offre una funzione con un protitpo particolare e quindi riesce a richiamarla senza bisogno di un header (nota che non abbiamo incluso "randutils.h" in "useawesome.c"). Tuttavia in librerie gigantesche (100 e passa funzioni e variabili) è impensabile generare una libreria senza generare anche i rispettivi header della libreria stessa!

La realtà è che questa non è una problematica di compilazione, ma è un problema di installazione della libreria. Per poter usare gli header della libreria è necessario che qualcuno prenda tutti gli header che la libreria definisce e li copi-incolli da qualche parte nel file system (possibilmente in un posto standardizzato). Si può quindi ben capire che, in generale, una libreria in C non è un singolo file nel file system, ma:

  • un file "*.a" or ".so" (a seconda del tipo di libreria);
  • una lista di tutti gli header che la libreria utilizza;

Successivamente quando lo sviluppatore va a creare un programma che sfrutta la libreria (sia statica che dinamica) egli andrà a includere l'header copiato nel posto standardizzato. Essendo standardizzato, il programmatore includerà l'header nel seguente modo:

#include <randutils.h>

Notare che da nessuna parte c'è scritto il nome della libreria che contiene "randutils.h". Se tu stessi usando 100 librerie dinamiche questo potrebbe essere un problema!

Nel compilare il programma che usa la libreria il compilatore andrà a cercare nella cartella del file system standardizzata, troverà "randutils.h" e potrà quindi includere con successo l'header. Se si usa un IDE, includendo l'header l'IDE sarà in grado di darvi la lista di funzioni utilizzabili.

Rimane però lo domanda: dove posso mettere i file header? E già che ci siamo, dove mettiamo il file "*.so" o "*.a" che rappresenta il codice macchina della libreria stessa? In realtà non esiste un processo standardizzatodi installazione che dice chiaramente dove e come mettere header e librerie. Comunque una buona soluzione è quella di:

  • mettere i file "*.a" e "*.so" in una cartella standardizzata (come "/usr/lib", "/lib" o la più sensata "/usr/local/lib"): in questo modo in fase di compilazione non si dovranno inserire flag -L;
  • mettere i file "*.h" in una sottocartella di una cartella standardizzata dal sistema (come "/usr/include", "/include" o la più sensata "/usr/local/include"). La sottocartella dovrebbe avere il "LINKER NAME" della libreria: in questo modo le direttiva "#include" che inseriscono gli header di una libreria particolare saranno ben visibili.

Seguendo questo approccio otteresti una situazione di questo genere nel file system (ipotizzando una libreria dinamica):

/usr
  /local
    /lib
      libmyawesomelib.so
    /include
      /myawesomelib
        randutils.h
        mathutils.h

Nell'eseguibile sfruttante la libreria avresti:

#include <myawesomelib/randutils.h>
#include <stdio.h>
#include <stdbool.h>

int main() {
    char buffer[100];
    int a, b;

    printf("choose first number: ");
    if (fgets(buffer, 100, stdin)) {
        sscanf(buffer, "%d", &a);
    }

    printf("choose second number: ");
    if (fgets(buffer, 100, stdin)) {
        sscanf(buffer, "%d", &b);
    }
    printf("a random number between %d and %d is %d\n", a, b, randRange(a, b, true, true));
}

Curiosità

Usiamo il programma nm per vedere che cosa cambia nell'eseguibile "useawesome" quando compiliamo con una libreria dinamica rispetto ad una statica. Eseguiamo le seguenti istruzioni (output disponibile in qui):

nm useawesomeShared > nm_shared
nm useawesomeStatic > nm_static

nm permette di capire quali sono i simboli macchina definiti in un eseguibile piuttosto che in una libreria. Se andiamo a fare un diff noteremo che nel caso dell'eseguibile con la libreria statica la funzione randRange è definita come "T", ossia all'interno dell'eseguibile stesso mentre con la libreria dinamica essa è definita con la flag "U", ossia che il riferimento è indefinito: ciò significa che questo riferimento deve essere popolato all'esecuzione del programma.

Se invece usiamo nm sulla libreria dinamica scoprimero che la funzione randRange è ben definita all'interno della libreria (marcata con "T").

Riferimenti

⚠️ **GitHub.com Fallback** ⚠️