Dynamic-link library (DLL)

In informatica, una dynamic-link library (termine inglese, tradotto in
italiano libreria a collegamento dinamico) è una libreria software che non
viene collegata staticamente ad un eseguibile in fase di compilazione, ma che
viene caricata dinamicamente in fase di esecuzione. Queste librerie sono note
con l’acronimo DLL, che è l’estensione del file che hanno nel sistema
operativo Microsoft Windows, o anche con il termine librerie condivise (da
shared library, usato nella letteratura dei sistemi Unix). Nei sistemi che
usano ELF come formato dei file eseguibili, come ad esempio Solaris o Linux,
sono anche note come “.so”, abbreviazione di Shared Object.

Vantaggi e svantaggi

La separazione del codice in librerie a collegamento dinamico permette di spezzare i programmi in parti concettualmente separate, che verranno caricate solo se effettivamente necessarie. Inoltre, una singola libreria può essere caricata in memoria una sola volta e utilizzata da più programmi, il che permette di risparmiare le risorse del sistema.

Un altro vantaggio è la possibilità di aggiornare un programma modificando solo le DLL: inserendo una versione diversa della DLL, che contiene ad esempio dei bug fix, tutti i programmi che la usano saranno automaticamente "aggiornati" senza bisogno di essere ricompilati.

Il principale svantaggio è legato al fatto che una nuova versione di una DLL, se mal progettata, potrebbe avere un comportamento diverso, interferendo in alcuni casi addirittura con il funzionamento di alcuni programmi che usavano la versione precedente. Ancora più critico il caso in cui un programma di installazione sovrascriva una DLL con una versione più vecchia. Questi problemi, ben noti ai programmatori, sono chiamati in gergo "DLL Hell" (inferno delle DLL).

In alcuni sistemi operativi, tipicamente Unix e Unix-like, è possibile far convivere versioni diverse fra loro incompatibili di una stessa libreria, purché entrambe siano presenti sul Filesystem in differenti percorsi e sia possibile in fase di collegamento del programma l'identificazione della versione corretta di libreria da utilizzare. In questa maniera i programmi collegati prima dell'installazione della nuova libreria possono continuare ad avvalersi della vecchia versione.[1]

I sistemi operativi Windows effettuano un back up periodico delle librerie .dll in un'apposita memoria cache. La copia delle librerie .dll di sistema viene effettuata nella cartella nascosta C:/Windows/system32/dllcache. Nella cartella system32/dll sono invece memorizzate le librerie in uso.

In mancanza di una libreria .dll, il sistema operativo o un programma in esecuzione restituiscono un messaggio di errore con il nome del file e la cartella dove deve essere copiato. La copia può essere ottenuta dalla cartella "dllcache", da un "CD di installazione" ovvero da un Internet.

Aprendo la libreria come file di testo, spesso, sono presenti al termine le istruzioni di installazioni che specificano il percorso e la cartella nella quale inserire la libreria.

Struttura e funzionamento

Quanto segue descrive la struttura ed il funzionamento di una libreria a collegamento dinamico in ambiente Windows, tuttavia i concetti espressi sono generalmente equivalenti in tutti i sistemi che permettono l'uso di librerie dinamiche.

Una libreria a collegamento dinamico è a tutti gli effetti un codice eseguibile. Ogni file eseguibile (EXE o DLL) dispone di un punto d'ingresso (entry point) invocato dal sistema operativo subito dopo il caricamento. Per una DLL il punto d'ingresso è mappato per convenzione sulla funzione DllMain (a discrezione, comunque, del compilatore).

La funzione DllMain viene invocata, oltre che al caricamento della DLL, anche allo scaricamento e quando un thread viene creato o distrutto nel processo in cui la DLL risiede.

A differenza di un file EXE, la DLL deve uscire dall'entry point non appena ha terminato le inizializzazioni necessarie.

Struttura

Per semplificare, possiamo pensare ad una libreria come ad una raccolta di funzioni. Ognuna di queste funzioni avrà il proprio indirizzo di base, calcolato come offset rispetto all'indirizzo di base assegnato dal sistema operativo durante il caricamento della libreria (vedi paragrafo successivo). Ciò che distingue una libreria dinamica è che queste funzioni possono essere esportate, ovvero i loro nomi vengono posti in una lista in una sezione dell'eseguibile. Perciò è possibile determinare il punto di ingresso di una funzione con una ricerca testuale basata sul nome della funzione. Questa operazione è svolta dall'API GetProcAddress che restituisce l'indirizzo della funzione il cui nome è passato come parametro.

Caricamento

Le librerie dinamiche vengono caricate dal sistema operativo all'interno dello spazio di memoria del processo che le ha richieste. In questo modo l'accesso al codice della DLL avrà prestazioni quasi equivalenti a quelle del codice dell'applicazione stessa o del codice delle librerie statiche (vedremo in seguito perché quasi equivalenti). Per evitare che il codice dell'applicazione e quello della DLL occupino la stessa posizione in memoria, il linker dovrà predisporre la DLL per la rilocazione. In pratica, il sistema operativo determina un'area di memoria disponibile e rimappa ogni riferimento alla memoria contenuto nella DLL. Siccome quest'operazione richiede tempo, ogni DLL dispone di un proprio indirizzo di base ideale: la rilocazione sarà necessaria solo se a questo indirizzo predeterminato è già stata mappata una precedente DLL. Per specificare l'indirizzo ideale si può usare una regola empirica, basata sulla lettera iniziale del nome della DLL, secondo la seguente tabella:

Lettera iniziale Indirizzo di base
A-C 0x60000000
D-F 0x61000000
G-I 0x62000000
J-L 0x63000000
M-O 0x64000000
P-R 0x65000000
S-U 0x66000000
V-X 0x67000000
Y-Z 0x68000000

Collegamento ad un eseguibile

Il collegamento di un eseguibile ad una libreria dinamica avviene durante l'esecuzione (a run time) ed avviene tramite l'API LoadLibrary, che accetta in input il nome della libreria. Ad esempio LoadLibrary(_T("MyLib.dll")) caricherà all'interno dello spazio di memoria dell'applicazione la DLL MyLib.dll. Il collegamento può essere di due tipi: esplicito o implicito.

Collegamento esplicito

Il collegamento esplicito viene gestito direttamente dal codice del programma con l'utilizzo delle due API LoadLibrary e GetProcAddress precedentemente descritte. Se si utilizza il linguaggio C si allocherà un puntatore alla funzione specificata nel quale, al momento di utilizzare la funzione richiesta, si caricherà l'indirizzo con GetProcAddress. Questa tecnica permette di gestire in modo appropriato la condizione nella quale una DLL richiesta non è presente nel sistema, ma in generale è più macchinosa perché richiede l'utilizzo esplicito delle due API. Questa tecnica è indispensabile usando alcuni linguaggi di programmazione, quali ad esempio Visual Basic.

Collegamento implicito

Il collegamento implicito è gestito direttamente dal linker in fase di compilazione, ed è usato quando si assume che una DLL sia sempre presente nel sistema. Ogni volta che nel codice sorgente è richiamata una funzione contenuta in una DLL, il linker collegherà la chiamata a funzione ad una funzione stub, ovvero ad una funzione fittizia. All'interno dell'eseguibile vi sarà una tabella contenente gli stub a tutte le funzioni di DLL richieste. In fase di caricamento dell'eseguibile, il sistema operativo caricherà in automatico tutte le DLL richieste e mapperà ogni stub al punto di ingresso della relativa funzione nella relativa DLL. Se una DLL (o anche una singola funzione in una DLL) richiesta non viene trovata il sistema operativo bloccherà l'avvio del programma con un messaggio di errore. L'utilizzo del collegamento implicito ha uno svantaggio in termini di prestazioni, perché ogni volta che viene richiamata una funzione contenuta in una DLL vi è un doppio salto a funzione: prima allo stub e poi all'indirizzo della funzione; l'overhead generato è in realtà trascurabile.

Collegamento implicito ritardato

Una variante al collegamento implicito prevista da alcuni compilatori è il collegamento ritardato. In questo caso viene utilizzato uno stub speciale, che non viene mappato al caricamento dal sistema operativo. Questo stub invece, la prima volta che verrà invocato, si mapperà automaticamente (con la tecnica del collegamento esplicito) alla funzione della DLL. Questa tecnica ha il vantaggio di non richiedere la presenza della DLL per il caricamento dell'eseguibile, insieme alla comodità di non dover caricare esplicitamente da codice la libreria.

Note

1. Sito HP, Manuale Digital Unix: Controllo versione librerie in fase di caricamento

Esempio

Per comprendere meglio come creare ed utilizzare le DLL, ho sviluppato un piccolo progetto di esempio, utilizzando Visual C++ (2003 o successivo).

L'esempio si compone di due progetti :

  1. testDLL
  2. testStart
Il primo rappresenta la vera e propria DLL, la quale espone due funzioni, una che verrà utilizzata sfruttando un collegamento implicito (fntestDLL), e una seconda che verrà richiamata usando un collegamento esplicito (fntestDLL2). Il secondo progetto serve per testare le funzioni presenti nella DLL, dimostrando come utilizzare collegamenti impliciti ed espliciti.

Iniziamo esaminando il codice sorgente presente nella DLL. In particolare, ci soffermeremo sui due file fondamentali, testDLL.cpp e testDLL.h, che definiscono come e cosa esportare della libreria.

Ecco il sorgente di testDLL.h:

1.  #ifdef TESTDLL_EXPORTS
2.  #define TESTDLL_API __declspec(dllexport)
3.  #else
4.  #define TESTDLL_API __declspec(dllimport)
5.  #endif
6.
7.  TESTDLL_API int fntestDLL(void);
8.
9.  extern "C" TESTDLL_API int fntestDLL2(void);

La prima cosa da notare è il gruppo di linee 1..5, qui viene definito un nuovo tipo (TESTDLL_API) e, in base alla presenza di TESTDLL_EXPORTS, sarà usato per l'esportazione o l'importazione delle funzioni/variabili/classi. Questo è utilile quando si andrà ad utilizzare questo file header all'interno dei progetti che useranno la DLL. Così facendo avremo un solo file .h condiviso tra il progetto dell DLL e i progetti che la utilizzano.

Alla riga 7 troviamo la dichiarazione della funzione 'fntestDLL'. Questa sarà poi utilizzata tramite una chiamata implicita all'interno del secondo progetto (testStart). La riga 9 invece, dichiara la funzione che sarà poi richiamata in modo espliciti in 'testStart'. Confrontando le due dichiarazioni si noterà che, l'unica differenza è la presena dell'istruzione 'extern', proprio questa consente l'uso di chiamate esplicite, la sua assenza impedisce l'uso di questa tecnica, costringendoci ad utilizzare una modalità di chiamata implicita.

Un'ultima menzione va fatta all'istruzione '__declspec' che, nella pratica, è quella che dice al linguaggio che una tal funzione/variabile/classe deve essere esportata o importata. Ovviamente la DLL esporta, mentre i progetti che la usano importano. Ne consegue che, nel progetto relativo alla DLL, la costante 'TESTDLL_EXPORTS' dovrà essere definita.

Ecco il sorgente di testDLL.cpp:

 1. #include "testDLL.h"
 2. 
 3. // Esempio di funzione esportata.
 4. TESTDLL_API int fntestDLL(void)
 5. {
 6. 	return 100;
 7. }
 8. 
 9. // Esempio di funzione esportata.
10. TESTDLL_API int fntestDLL2(void)
11. {
12. 	return 200;
13. }

Analizzando questo file non si nota nulla di particolare, infatti vengono semplicemente riportate le definizioni delle funzioni esportate dalla DLL.

Finito. La DLL è tutta qui. Un file .h contenente le dichiarazioni e un file .cpp con le definizioni.

Passiamo ora ad esaminare i file del secondo progetto, testStart. In questo caso osserveremo un solo file.

Ecco il sorgente di testStart.cpp:

 1. #include "stdafx.h"
 2. #include "windows.h"
 3. 
 4. #include "..testdlltestDLL.h"
 5. 
 6. #include 
 7. #include 
 8. 
 9. using namespace std;
10. 
11. typedef int (WINAPI* FNTESTDLL2_EXP) ();
12. 
13. //esegue la funzione 'fntestDLL', collegata in modo implicito
14. void EseguiImplFn (void)
15. {
16. 	cout << "Valore ritornato da funzione collegata implicitamente 
(fntestDLL) : " << fntestDLL() << endl;
17. }
18. 
19. //esegue la funzione 'fntestDLL2', collegata in modo esplicito
20. int EseguiEsplFn (void)
21. {
22. 	HMODULE hMod = LoadLibrary(_T("testDLL.dll"));
23. 	if (hMod == NULL)
24. 	{
25. 		cout << "nLoadLibrary fallito!n";
26. 		cin.get();
27. 		return 1;
28. 	}
29. 
30. 	FNTESTDLL2_EXP fntestDLL2_Exp = (FNTESTDLL2_EXP) GetProcAddress 
(hMod, "fntestDLL2");
31. 	if (!fntestDLL2_Exp)
32. 	{
33.       FreeLibrary(hMod);
34. 	  cout << "nGetProcAddress fallito!n";
35. 	  cin.get();
36.       return 1;
37.     }
38. 
39. 	cout << "Valore ritornato da funzione collegata esplicitamente 
(fntestDLL2) : " << fntestDLL2_Exp() << endl;
40. 	FreeLibrary(hMod);
41. 	return 0;
42. }
43. 
44. int _tmain(int argc, _TCHAR* argv[])
45. {
46. 
47. 	EseguiImplFn();
48. 	EseguiEsplFn();
49. 	cin.get();
50. 	return 0;
51. }

Questo progetto produce un file eseguibili in modalità console, il quale ha il compito di visualizzare i valori interi restituiti dalle funzioni presenti nella nostra DLL.

Analizzando il file di programma, la prima cosa da notare è la presenza, alla riga 4, dell'inclusione del file header relativo alla DLL, lo stesso utilizzato nel primo progetto. Questo, unito al fatto che verra aggiunta, nella fase di link, la libreria 'testDLL.lib' (generata dalla compilazione della DLL), ci permetterà di richiamare le funzioni in modo implicito.

Tralasciando il codice standard, proprio del C++, rivolgiamo la nostra attenzione alla linea 11. Qui troviamo la dichiarazione del tipo FNTESTDLL2_EXP, il quale rappresenta la struttura della funzione (presente nella DLL) che utilizzeremo in modo esplicito. Ogni funzione richiamata in modo esplicito richiede la definizione della sua struttura, nel nostro caso una funzione senza parametri che ritorna un valore intero.

Saltiamo ora, alla funzione principale del programma di test, alla linea 44. Qui vediamo che vengono utilizzate le due funzioni 'EseguiImplFn' e 'EseguiEsplFn' che, rispettivamente, richiamano la DLL in modo implicito e in modo esplicito.

Esaminiamo, prima di tutto, la più complessa, cioè 'EseguiEsplFn' alla riga 20, che effettua una chiamata esplicita. Prima di tutto viene caricata la nostra DLL usando l'API 'LoadLibrary', una volta verificato il corretto caricamento, si ottiene il puntatore alla funzione da utilizzare (presente nella DLL) con 'GetProcAddress'. Proprio in ques'ultima operazione si fa uso del tipo FNTESTDLL2_EXP che dice al compilatore come dovrà essere trattato il puntatore ottenuto. Infine si utilizza questo puntatore come fosse una normale funzione alla riga 39 (fntestDLL2_Exp()).

Passiamo ora ad esaminare la chiamata implicita, effettuata dalla funzione 'EseguiImplFn', alla riga 14. In realtà non bisogna fare nulla di particolare, è sufficiente usare la funzione richiesta 'fntestDLL()'. Questo è possibile in quanto, allinterno del file 'testDLL.h' è presente la sua definizione, mentre la libreria (testDLL.lib) aggiunta in fase di compilazione effettua il collegamento con il nostro file 'testDLL.dll'.

Dopo aver fatto un po di noiosa teoria, passiamo alla pratica.
Download Per scaricare il sorgente della DLL e del programma di test, clicca qui.



Questo articolo, ad esclusione della sezione 'Esempio' è tratto da Wikipedia , ed è disponibile nel rispetto dei termini della GNU Free Documentation License.