Shared Object e Loading Dinamico in Linux

Nell’articolo precedente avevamo trattato del link statico e dinamico con le DLL, questa volta parleremo degli shared object e del loading dinamico. Le librerie di per sé non riescono a risolvere un problema, l’occupazione dello spazio in memoria, infatti ogni volta che un eseguibile è caricato dal loader del sistema operativo ( o da chi per esso ), devono essere soddisfatte tutte le dipendenze associando tutti i riferimenti simbolici esterni, per questo le DLL consentono di risparmiare spazio su disco, ma non in memoria. Gli shared object, comunemente chiamate librerie condivise, sono pregettati per consentire la condivisione della loro parte testo tra i processi che ne dipendono, solamente la sezione .data, che contiene i dati inizializzati del modulo software, è copiata in uno spazio di ogni processo dipendente. Per fare in modo di poter condividere la memoria ci si avvale di meccanismi di IPC a memoria condivisa; la qual cosa implica che gli indirizzi di mapping tra i vari processi possono essere differenti. Per questo motivo è necessario che il codice della libreria generato dall’assembler e successivamente rilocato dal link editor, sia Position Indipendent Code ( PIC ) e cioé indipendente dalla particolare posizione in memoria e referenziabile dai singoli processi anche se questi lo agganciano con indirizzi di memoria differenti. Ad esempio la chiamata alla funzione printf che risiede nella libreria standard ( glibc ) di Linux, in pratica è chiamata per indirizzo e cioé call indirizzo-memoria ed è necessario che tale istruzione sia aggiornata con gli effettivi indirizzi di mapping della libreria per il particolare processo. Nei moderni sistemi Unix-like ogni modulo software dipende da una libreria condivisa che possiede due particolari sezioni, la Global Offset Table ( .got ) e la Procedure Linkage Table ( .plt ). Ogni riferimento della libreria condivisa è tradotto in chiamata alla .plt che a sua volta passa il controllo alla .got; è compito del loader aggiornare i campi della .got con gli effettivi indirizzi di mapping della libreria condivisa. In verità in Linux esistono anche altri metodi per il caricamento degli shared object, come ad esempio con la funzione uselib(). Ogni libreria condivisa contiene due sezioni, .start e .fini, a cui il loader passa il controllo rispettivamente per la fase di loading e di unloading della libreria stessa.
Finora abbiamo visto la fase di linking e loading di un’applicazione effettuata a tempo di compilazione, ma potrebbe essere utile ritardare la fase di linking quando se ne ha bisogno a runtime, per rudurre l’utilizzo di memoria e cpu, il cosiddetto loading dinamico; vediamo come implementarlo attrraverso l’uso della libreria libdl. Questa libreria sviluppata da SUN per il suo sistema operativo Solaris, implementa 4 funzioni principali:

  • dlopen – effettua il linking a run-time e ritorna l’handle della libreria collegata
  • dlsym – restituisce il puntatore alla funzione richiamata
  • dlclose – effettua lo sganciamento al modulo
  • dlerror – se una delle altre 3 funzioni fallisce questa funzione ritorna la descrizione dell’errore

Vediamone l’uso creando una libreria dinamica con una semplice funzione, la chiameremo hello.c

#include "stdio.h"

void sayhello(char* strWho)
{
   printf("Ciao a tutti da %s\n", strWho);
}

Creiamo la libreria con questo comando

gcc -fPIC -shared -o libhello.so hello.c

A questo punto scriviamo il programma principale, main.c, che richiama la funzione sayhello della libreria libhello.so tramite la libreria libdl.

#include "dlfcn.h"

int main(int argc, char* argv[])
{
      void* module;
      void (*run) (char*);

      if ((module = dlopen("./libhello.so", RTLD_NOW)))
     {
            if ((run = dlsym(module, "sayhello")))
                (*run)("Giampaolo Rossi");
           else
                error(dlerror());
           dlclose(module);
      }
      else
          error(dlerror());
}

Infine creiamo il file principale che chiameremo say con questo comando

gcc -rdynamic -o say main.c -ldl

Lanciando il programma ./say apparirà la scritta che risiede nella funzione sayhello.
Come abbiamo potuto notare in questo articolo, il linking è la fase cruciale della creazione di un applicativo, conoscerlo a fondo può aiutare ad affrontare problemi come la mancanza delle dipendenze e la compilazione ed installazione di pacchetti disponibili solo in formato sorgente. Ricordo che per qualsiasi domanda potete rivolgervi al nostro forum su Linux.

Di Giampaolo Rossi

Sviluppatore software da oltre 16 anni.