Tutorial 17: Les DLL
(Dynamic Link Libraries)
Bibliothèques de Liaison, Dynamiques
Dans ce Tutorial, nous allons voir les DLLs, ce qu'elles sont et comment les créer.
Vous pouvez télécharger l'exemple ici.
Théorie:
Si vous programmez depuis un petit moment déjà, vous constaterez que les programmes que vous avez l'habitude d'écrire ont quelques routines de code en communs. C'est vraiment une perte de temps de les récrire à chaque fois que vous commencez à développer de nouveaux programmes. Revenons en arrière, aux vieux jours du DOS, les programmeurs ont faits en sorte de stocker les routines les plus généralement employées dans une ou plus bibliothèques. Quand ils veulent utiliser les fonctions, ils ont juste à lier la bibliothèque avec le fichier d'objet, et le linker extrait les fonctions de la bibliothèque et les insère dans le fichier final exécutable. Ce processus est appelé la jonction statique. Les bibliothèques 'Runtime du C' sont de bons exemples. L'inconvénient de cette méthode c'est que vous avez des fonctions identiques dans chaque programme qui les appelle. Votre espace disque est gaspillé stockant plusieurs copies identiques des fonctions. Mais pour les programmes sous DOS, cette méthode est tout à fait valable puisqu'il n'y a généralement qu'un seul programme en action dans la mémoire. Ainsi il n'y a aucune perte précieuse de mémoire.
Sous Windows, la situation devient beaucoup plus critique parce que vous pouvez avoir plusieurs programmes tournant simultanément. La mémoire sera rapidement saturée si votre programme est vraiment grand. Windows a une solution pour ce type de problème : les bibliothèques de liaison dynamiques. Une bibliothèque de liaison dynamique est une sorte de regroupement commun de plusieurs fonctions. Windows ne chargera pas en mémoire plusieurs copies d'une dll, même dans le cas où il y a beaucoup de programmes tournant en même temps. Il fera seulement une copie du dll que le programme emploie dans la mémoire. Et je dois clarifier un peu ce point. En réalité, tous les processus qui emploient la même dll, auront leurs propres copies de cette dll. C'est comme si il y avait plusieurs copies de cette dll en mémoire. Mais en réalité, Windows rend ça magique avec la pagination et tous les morceaux de processus ayant le même code dll. Ainsi en mémoire physique, il y a uniquement une copie du code de la dll. Cependant, chaque process aura sa propre section de données unique du dll.
Le programme se lie avec une DLL pendant l'exécution, à la différence de la vieille bibliothèque statique. C'est pourquoi on appelle la bibliothèque de liaison dynamique. Vous pouvez aussi vous défaire d'une DLL pendant l'exécution quand vous n'en avez plus besoin. Si ce programme est le seul à employer la DLL, il la virera immédiatement de la mémoire. Mais si la DLL est toujours employée par d'autres programmes, la DLL reste en mémoire tant que le dernier programme qui l'emploie s'en défasse.
Cependant, le linker a un travail plus difficile quand il exécute l'adresse fixups pour le fichier final exécutable. Puisqu'on ne peut pas "extraire" les fonctions et les insérer complètement dans le fichier final exécutable, d'une façon ou d'une autre on doit stocker assez d'informations sur la DLL et ses fonctions dans le fichier executable final pour être capable de charger correctement la DLL pendant l'exécution.
C'est là ! que la bibliothèque d'importation entre en jeu. Une bibliothèque d'importation contient les informations de la DLL qu'il représente. Le linker peut extraire des bibliothèques d'importation les renseignements dont il a besoin. Quand le loader Windows charge le programme en mémoire, il voit que le programme se lie avec une DLL donc il essai de trouver cette DLL et la réécrit aussi dans l'espace d'adresse du process(du programme), puis exécute les adresses hors du programme pour les fonctions Call qui appellent la DLL.
Vous pouvez vouloir charger la DLL vous-même sans compter sur le loader (chargeur) de Windows. Cette méthode a son pour et son contre :
-
Il n'y a pas besoin d'une bibliothèque d'importation donc vous pouvez charger et employer n'importe qu'elle DLL même si elle n'est accompagnée d'aucune bibliothèque d'importation. Cependant, vous avez quand même besoin de savoir certaines choses sur les fonctions qu'elle vous propose, des choses comme combien de paramètres elles utilisent…
-
Si vous laissez le loader charger la DLL pour votre programme, et que le loader ne trouve pas cette DLL il annoncera "Le fichier .DLL exigé, xxxxx.DLL est manquant" et paf! Votre programme n'a aucune chance d'être lancé même si cette DLL n'est pas essentiel pour son fonctionnement minimal. Si vous chargez le DLL vous-même, et que la DLL ne peut pas être trouvé et que ce n'est pas essentiel pour l'opération, votre programme le fait remarquer à l'utilisateur mais peut continuer.
-
Vous pouvez utiliser et appeler d'autres fonctions *non décrites dans ces Tuts* qui ne sont pas incluses dans les bibliothèques d'importation. À condition que vous connaissiez assez de renseignements sur ces fonctions.
-
Si vous utilisez LoadLibrary, vous devez appeler GetProcAddress pour chaque fonction que vous souhaitez appeler. GetProcAddress retrouve l'adresse de l'entrypoint d'une fonction à l'intérieur d'une DLL particulière. (l'addresse où commence une fonction particulière). Donc il se peut que votre code soit un peu plus grand et plus lent, mais ça change pas grand chose.
En comparant les avantages / inconvénients d'un appel à LoadLibrary, nous entrons maintenant dans les détails pour créer une DLL.
Le code suivant est le squelette de la DLL. (DLL skeleton.)
;--------------------------------------------------------------------------------------
;
DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model
flat,stdcall
option
casemap:none
include
\masm32\include\windows.inc
include
\masm32\include\user32.inc
include
\masm32\include\kernel32.inc
includelib
\masm32\lib\user32.lib
includelib
\masm32\lib\kernel32.lib
.data
.code
DllEntry
proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry
Endp
;---------------------------------------------------------------------------------------------------
;
C'est une fonction factice
;
Ça ne fait rien. Je l'ai mise ici pour montrer où vous pouvez insérer des fonctions à l'intérieur d'une DLL.
;----------------------------------------------------------------------------------------------------
TestFunction
proc
ret
TestFunction
endp
End
DllEntry
;-------------------------------------------------------------------------------------
;
DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY
DLLSkeleton
EXPORTS
TestFunction
Ce tout petit programme ci-dessus est le squelette de la DLL. Chaque DLL doit avoir une fonction d'entrypoint. Windows appellera la fonction d'entrypoint chaque fois que :
-
La DLL est loadée en premier
-
La DLL est unloadée (libérée)
-
Un lien est créé dans ce même process
-
Un lien est détruit dans ce même process
DllEntry
proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry
Endp
Vous pouvez nommer la fonction d'entrypoint comme vous le souhaitez tant que vous avez une FIN correspondante à 'END ' . Cette fonction prend trois paramètres, dont seulement les deux premier sont importants.
hInstDLL
est l'handle du module(de la fonction) de la DLL. Ce n'est pas le même que l'instance handle du process. Vous devez garder cette valeur si vous avez besoin de l'employer plus tard. Vous ne pourrez pas l'obtenir de nouveau aussi facilement.
reason
peut être une des quatre valeurs :
-
DLL_PROCESS_ATTACH
La DLL reçoit cette valeur quand il est injecté la toute première fois dans l'espace d'adresses du process. Vous pouvez utiliser cette occasion pour faire l'initialisation.
-
DLL_PROCESS_DETACH
La DLL reçoit cette valeur quand elle est unloadée (supprimée) de l'espace d'adresses du process. Vous pouvez employer cette occasion pour faire un peu de nettoyage comme désinffecter la mémoire etc…
-
DLL_THREAD_ATTACH
La DLL reçoit cette valeur quand le process crée un nouveau lien.
-
DLL_THREAD_DETACH
La DLL reçoit cette valeur quand un lien dans le process est détruit.
Renvoyez TRUE dans eax si vous souhaitez que la DLL continue son exécution. Si vous renvoyez FALSE, la DLL ne sera pas chargé. Par exemple, si votre code d'initialisation doit réserver de la mémoire et qu'il ne peut pas faire ça avec succès, la fonction d'entrypoint doit renvoyer FALSE pour indiquer que la DLL ne peut pas être lancée.
Vous pouvez placer vos fonctions dans la DLL après la fonction Entrypoint ou même avant çà. Mais si vous voulez qu'elles soient CALLables par d'autres programmes, vous devez mettre leurs noms dans la liste d'exportation du fichier de définition de module (.def).
Une DLL a besoin d'un fichier de définition de module dans son processus liée à son développement. Nous y jetterons un coup d'œil maintenant.
LIBRARY
DLLSkeleton
EXPORTS
TestFunction
Normalement vous devez avoir la première ligne. La déclaration LIBRARY définit le nom du module interne (la fonction souhaitée) de la DLL. Vous devez correspondre avec ce module grâce au nom du fichier de la DLL.
La déclaration EXPORTS dit au linker quelles fonctions sont en train de tourner dans la DLL, c'est-à-dire quelles fonctions sont déjà utilisées par d'autres programmes. Dans l'exemple, nous voulons que d'autres modules soient capables d'appeler TestFunction, donc nous mettons son nom dans la déclaration EXPORTS.
Un autre changement c'est le commutateur du linker. Vous devez mettre le commutateut /DLL et /DEF:<votre nom de fichier def>
dans votre linker de commutation comme cela :
link
/DLL /SUBSYSTEM:WINDOWS
/DEF:DLLSkeleton.def
/LIBPATH:c:\masm32\lib DLLSkeleton.obj
Les commutateurs d'assembleur sont les mêmes, à savoir /c /coff /Cp. Ainsi après que vous ayez lié le fichier objet, vous obtiendrez *.DLL et *.lib. Le *.lib est la bibliothèque d'importation que vous pouvez employer pour vous lier avec d'autres programmes qui emploient les fonctions de votre DLL.
Ensuite je vous montrerai comment employer LoadLibrary pour charger une DLL.
;---------------------------------------------------------------------------------------------
;
UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model
flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0
.data?
hLib dd ?
; l' handle de la librairie (l' handle de la DLL)
TestHelloAddr dd ?
; l'adresse de la function TestHello
.code
start:
invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
;
Appelle LoadLibrary avec le nom de la DLL désirée. Si l'appel est couronné de succès
;
Il renverra l'handle de la bibliothèque (DLL). Sinon, il renverra le NULL.
;
Vous pouvez passer l'handle de la bibliothèque à GetProcAddress ou n'importe quelle fonction qui en a besoin
;
de l'handle d'une DLL comme paramètre.
;------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;-------------------------------------------------------------------------------------------------------------
;
Quand vous obtenez l'handle de la bibliothèque, vous le passez à GetProcAddress avec l'adresse
;
du nom de la fonction de la DLL que vous souhaitez appeler. Ceci renvoie l'adresse
;
de la fonction si tout s'est passé avec succès. Autrement, il renvoie le NULL.
;
Les adresses des fonctions ne changent pas à moins que vous ne déchargiez puis rechargiez la bibliothèque.
;
Donc vous pouvez les placer dans des variables globales pour de futures utilisations.
;-------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;-------------------------------------------------------------------------------------------------------------
;
Ensuite, vous pouvez appeler la fonction avec un appel simple avec la variable contenant
;
l'adresse de la fonction comme l'opérande.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;-------------------------------------------------------------------------------------------------------------
;
Quand vous n'avez plus besoin de la bibliothèque, vous la déchargez avec FreeLibrary.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end
start
Donc vous pouvez voir que l'utilisation de LoadLibrary est un peu plus impliquée mais c'est aussi plus flexible.
[Iczelion's Win32 Assembly Homepage]
Traduit par Morgatte