| Indice Articolo |
|---|
| Creazione di una DLL |
| Gestione del caricamento e rilascio |
| Parametri complessi |
| Da eseguibile a libreria |
| Debug di una DLL |
| Tutte le pagine |
In questo articolo studieremo la creazione di una DLL con Delphi.
I file DLL come anche altri tipi di librerie e come i file eseguibili hanno una struttura PE. Al loro interno quindi avranno le stesse tabelle ed gli stessi header dei file eseguibili, e quindi anche un elenco delle librerie importate,con le rispettive funzioni e una elenco delle proprie funzioni esportate. Quest'ultimo è molto importante per una libreria, dato che le funzioni esportate solitamente rappresentano la sua ragione di esistere. Infatti potremo compilare una DLL con tutte le sue funzioni, ma se poi non le esportiamo la libreria sarebbe inutile.
Quindi in un semplice schema avremo una parte di implementazione ed alla fine delle istruzioni di esportazione, mentre in un loader di test importeremo le funzioni della DLL semplicemente dichiarandole come "external":
| Libreria |
Loader |
| library CreateLibrary1; //dichiarazione di funzioni e procedure function add(a,b:integer):integer; begin result:=a+b; end; //esempio di passaggio di parametri per indirizzo procedure sub(a:integer;var b:integer); begin b:=a-b; end; //esportazione funzioni exports add,sub; begin end. |
program LoaderTest; {$APPTYPE CONSOLE} uses SysUtils; //dichiarazione funzioni per l'importazione statica function add(x,y:integer):integer; external 'CreateLibrary1.dll' procedure sub(x:integer;var y:integer); external 'CreateLibrary1.dll' var a,b:integer; begin writeln('Prova dll:'); a:=add(3,5); write('3+5 = '); writeln(a); b:=1; sub(7,b); write('7+1 = '); write(b); end. |
In questo esempio abbiamo esportato una normale funzione ed una procedura, la quale è semplicemente una funzione che non restituisce alcun valore. In particolare abbiamo introdotto in quest'ultima il passaggio di parametri per indirizzo, che ricordo implica il passaggio non del valore da dare in pasto alla procedura, ma dell'indirizzo in memoria della variabile che lo contiene. La procedura nel codice della libreria andrà a modificare direttamente questa area di memoria, eventualmente sovrascrivendola.
E' molto importante dichiarare la funzione nel modo corretto, dato che il compilatore pretende solo che vi sia una dichiarazione e non va a verificare la sua correttezza, dato che non potrebbe farlo non avendo a disposizione il suo codice. Infatti se per esempio avessimo dichiarato la procedura in questo modo:
procedure sub(x,y:integer); external 'CreateLibrary1.dll'
il compilatore non avrebbe comunicato alcun errore, ma in fase di esecuzione ecco cosa accade:
Indirizzo codice istruzione
//b:=1;
004091A6 BB01000000 mov ebx,$00000001
//sub(7,b);
004091AB 8BD3 mov edx,ebx
004091AD B807000000 mov eax,$00000007
004091B2 E899F9FFFF call sub
viene usato il registro EBX come variabile locale b , mentre la funzione sub viene chiamata servendogli i due parametri come una qualsiasi FASTCALL sui due registri EAX ed EDX. Dopo di che si passa alla tabella delle funzioni che rimanderà direttamente alla funzione dentro la libreria:
Indirizzo codice istruzione
//b:=a-b;
003F3B18 2B02 sub eax,[edx]
003F3B1A 8902 mov [edx],eax
//end;
003F3B1C C3 ret
e qui iniziano i problemi, infatti la prima istruzione si potrebbe tradurre così : "sottrai al valore contenuto in EAX il valore che c'è nella posizione indicata nel registro EDX", ma il registro EDX contiene il valore 00000001 il quale sarebbe dovuto essere invece il valore da sottrarre e non il suo indirizzo. Quindi verrà sollevata una eccezione che porterà alla terminazione del programma.
Quindi se usiamo lo stesso compilatore per creare la libreria e per creare il programma che la dovrà caricare, in entrambi sarà necessario e sufficiente dichiarare lo stesso numero e lo stesso tipo di parametri e se necessario lo stesso tipo di risultato.
Ma se usiamo un compilatore diverso, oltre all'equivalenza formale, dovremo assicurarci anche che corrisponda il tipo di chiamata! Infatti avendo due variabili di tipo intero il compilatore delphi predisporrà una fastcall sia nella libreria che nel programma che la importa, ma se quest'ultimo non è fatto in delphi allora non potrebbe usare la nostra libreria, perché nella stessa situazione probabilmente userebbe un altro tipo di chiamata. Per evitare questi problemi nelle funzioni di libreria è meglio usare esplicitamente il tipo di chiamata StdCall (standard call), in questo modo le librerie potrebbero essere usate anche da programmi compilati in c o in altri linguaggi. Lo stesso vale se noi dovessimo importare in una applicazione delphi delle funzioni di librerie fatte in un altro linguaggio. Per chi non sapesse cosa sono le modalità di chiamata di una funzione vi consiglio di leggere l'articolo sullo studio delle funzioni.
Un altro aspetto della dichiarazione nel loader è la modalità del richiamo della DLL, infatti il loader saprà dove trovare la funzione grazie al nome della DLL che avevamo specificato dopo la parola chiave external, inoltre è possibile richiamare la funzione dalla DLL tramite un indice:
function add(x,y:integer):integer; external 'CreateLibrary1.dll' index 2
procedure sub(x:integer;var y:integer); external 'CreateLibrary1.dll' index 1
senza l'uso degli indici il loader cercherà le funzioni da importare tramite il loro nome, mentre in presenza di un indice darà la priorità a quest'ultimo. Nella DLL creata precedentemente, anche senza dichiararli esplicitamente, sono stati assegnati due indici alle due funzioni esportate:
| Indice |
Funzione |
| 1 |
sub |
| 2 |
add |
ora se io dichiarassi le funzioni scambiando gli indici:
function add(x,y:integer):integer; external 'CreateLibrary1.dll' index 1
procedure sub(x:integer;var y:integer); external'CreateLibrary1.dll' index 2
alla funzione add nel mio programma corrisponderà la funzione sub della DLL e la funzione sub corrisponderà la funzione add della libreria. Quindi è consigliato usare gli indici solo avendo la sicurezza della corretta corrispondenza con le funzioni.
Da un punto di vista della creazione della libreria è possibile assegnare esplicitamente gli indici e i nomi tramite le parole chiave index e name e quindi la sezione riguardante le esportazioni diventerà:
| Esportazione implicita |
Esportazione esplicita |
| exports add,sub; |
exports add index 2 name 'add' |
| exports sub index 1 name 'sub' |
naturalmente con la direttiva "name" è possibile impostare il nome della funzione che verrà esportata, quindi se esportassi la funzione in quest modo:
exports add index 2 name 'sum'
da un programma esterno potrò importare solamente la funzione col nome sum e non più col nome add.





