Vous allez tout comprendre sur les DLL... :)
Dependency Walker est un utilitaire pour l'exploration 32 bits des tables (statiques) d'importation et d'exportation d'un fichier exécutable. Vous me direz que ce n'est pas un utilitaire pour faire de la programmation, mais je répondrais que c'est un tool qui est indispensable dans ce domaine. Il va en plus nous ammener à faire quelques considérations.
Il permet entre autre :
Accrochez-vous bien...
Note : Lors de la création de ce tutoriel, la version disponible était la 1.0. Cette note importante avant toute utilisation semble ne plus concerner les versions récentes (2.2).
Cet utilitaire comporte un seul fichier et disponible ICI :
DEPENDS.EXE
!!!! NOTE ULTRA IMPORTANTE !!!!
Vous ne pourrez pas dire que je vous ai caché des choses
- Si vous ne comptez pas utiliser ce logiciel
- S'il ne vous intéresse pas particulièrement
- Si vous avez le moindre doute sur ce produit
- Si vous êtes un grand débutant
- ...
ALORS N'EXÉCUTEZ PAS L'APPLICATION !!!!!!!!!
La raison est très simple : ce logiciel fait des inscriptions dans la base de registre afin d'ajouter dans l'explorateur des items "Dependency Walker" sur les fichiers EXE, DLL, SCR, VXD, DRV... bref, la totale sur tous les fichiers de format exécutable. Ces inscriptions ont lieu à chaque fois que DW est lancé, et à ma connaissance, il n'est pas possible d'éliminer automatiquement ces traces. Manuellement, ça se fait facilement, mais c'est prise de tête... long et chiant !
Cependant, que cela ne vous empêche pas de lire ce tutoriel, histoire d'apprendre quelques trucs sympathiques. Je vous donne de toute manière des aperçus d'écran (!!! version 1.0) pour que vous voyiez les choses sans nécessairement utiliser le logiciel.
J'ai choisi de lancer l'application et depuis que je l'utilise, je n'ai jamais eu le moindre problème. Voici ce qui s'affiche en analysant le fichier c:\windows\explorer.exe dans sa version pour Windows XP :
Cette image peut paraître illisible, mais les informations proposées sont très précieuses.
On remarque 4 zones principales ayant chacune un but précis.
Vous avez en première ligne le fichier analysé.
Le niveau juste inférieur donne toutes les DLL dont l'application a strictement besoin pour s'exécuter.
Le niveau encore inférieur sont les ressources utilisées par lesdites DLL.
Or, DW se présente comme un «Recursive Module Dependency Checker». On ne va pas analyser les ressources des DLL de l'EXE, car sinon on rentrerait dans une boucle sans fin. Pour y remédier DW vous donne le logo : qui indique qu'il faut que vous vous référiez aux données des niveaux supérieurs (données nécessairement disponibles). On remarque au passage sur la capture qu'il existe un sous-niveau 3 repéré par le fichier RPCRT4.DLL. Comme ce fichier est à ce stade encore inconnu, DW se charge d'afficher ses dépendances. Naturellement, si ce fichier est rencontré dans des niveaux encore plus profonds, alors vous visualiserez
En faisant un clic droit dans l'arborescence, vous pouvez avoir accès aux propriétés de la ressource.
Cette section affiche toutes les fonctions et procédures utilisées par la ressource parente de la sélection. Par exemple, sur la capture, vous voyez ce que EXPLORER.EXE vient piocher dans MSVCRT.DLL. Dependency Walker vous donne leur position dans la table d'importation, leur nom et enfin leur point d'entrée (on va voir après ce que c'est).
Le "i" encadré en vert signifie "importation".
Ce cadre montre toutes les ressources disponibles dans la ressource sélectionnée dans le cadre N°1.
Le "e" encadré en vert signifie "exportation".
Enfin, ce cadre (aligné en bas de l'écran) résume les propriétés logicielles de toutes les ressources qui ont été trouvées dans le cadre N°1. Vous avez la date, la plateforme, la machine (c'est presque toujours Intel x86), les versions...
Que ce soit GUI ou Console, ça dépend si une console (l'écran noir style MsDos) peut être utilisée.
Avant de finir, lancez DW sur le fichier c:\windows\kernel32.dll (pour Windows 98) et sur c:\windows\system32\kernel32.dll (pour Windows XP). Les dépendances sont bien maigres et c'est normal pour un fichier noyau de cette importance.
Prenons la fonction IntPower de l'unité MATH.PAS de Delphi. Nous avons ceci :
function IntPower(Base:extended; Exponent:integer):extended; asm MOV ecx, eax CDQ FLD1 XOR eax, edx SUB eax, edx JZ @@3 FLD Base JMP @@2 @@1: FMUL ST, ST @@2: SHR eax,1 JNC @@1 FMUL ST(1),ST JNZ @@1 FSTP st CMP ecx, 0 JGE @@3 FLD1 FDIVRP @@3: FWAIT end;
Même si nos chers mathématiciens calculent simplement x^y, vous voyez que les ordinateurs font bien différemment et c'est compliqué. Je me dois d'expliquer quelque peu sur ce que signifient toutes ces commandes barbares.
Déjà, notre fonction est en assembleur et repérée par le mot clé ASM qui fait office d'un BEGIN. Elle est composée de commandes élémentaires et irréductibles qui se succèdent les unes derrière les autres. On a la syntaxe suivante :
[INSTRUCTION] [Paramètre 1] , [Paramètre 2] , ... , [Paramètre N]
Les marqueurs @@ correspondent à des LABEL. C'est pareil que le LABEL et GOTO de Delphi, chacun devenant une adresse et un JUMP (conditionnel ou pas) après compilation.
Il est impossible d'avoir une instruction dans le paramétrage d'une autre instruction. C'est à dire que...
MOV EAX, 5 INC EAX
... ne peut pas devenir ce qui suit, sachant que le registre de sortie de la première instruction est EAX.
INC MOV EAX, 5
Les instructions ASM sont équivalentes en Delphi à une succession de procédures : les function sont interdites. Par exemple ...
var i : integer ; function Resultat(Value:integer):integer; begin Resultat := Value+5; end; begin i:=20; i:=Resultat(i); //i vaut alors 25 end.
... deviendrait plutôt ceci :
var i : integer ; procedure Resultat; begin i := i+5; end; begin i:=20; Resultat; //i vaut alors 25 end.
C'est un peu moins lisible, car il faut se souvenir où le "résultat" de la procédure est stocké. Le rôle que joue la variable i dans cet exemple en Delphi, est celui des registres en assembleur.
Autre exemple tiré de l'aide de Delphi : additionner deux variables et renvoyer le résultat.
function Sum(var X, Y: Integer): Integer; pascal; asm MOV EAX,X MOV EAX,[EAX] MOV EDX,Y ADD EAX,[EDX] MOV @Result,AX end;
Même en ASM, il y a des pointeurs à tout va. Regardez le tuto de CptPingu si vous avez le moindre doute sur les pointeurs. C'est très bien fait...
Les DLL sont des librairies qui référencent en leur sein pleins de fonctions. Mais pour appeler ces fonctions, il faut repérer leur position de départ qui est généralement la première instruction codant pour ladite fonction.
Delphi est un langage compilé, c'est à dire que c'est Windows qui se charge d'exécuter les applications. Il ne faut pas confondre avec les langages interprétés qui nécessitent d'installer un logiciel spécifique de lecture (souvent compatible sur différents OS, ce qui dit faussement que les langages interprétés sont multiplateformes [l'universel n'existant bien sûr pas]). Par exemple, les applets Java ne sont pas des applications Win32. La structure interne de ces fichiers est spécifique à un interpréteur, d'où la nécessité d'installer ce dernier.
Malgré la diversité des langages de programmation, leur point commun est le fichier exécutable qui n'est ni plus ni moins qu'un fichier écrit en assembleur compilé. Disons que c'est de l'assembleur pur édulcoré autour pour créer un tout exécutable sous Windows. Par exemple, la vérification de la plateforme (vous savez le fameux message "This application cannot be run under MsDos mode") est un ajout de la compilation. On note également que les applications Visual Basic rajoutent même un lancement de Windows via WIN.COM au lieu d'afficher ce message.
Tous les linkers font de la conversion en langage assembleur, d'où la nécessité d'une syntaxe cohérente, structurée et logique. L'appellation langage de haut niveau dépend de la difficulté de cette traduction.
Sur un PC, vous avez un processeur Pentium qui fonctionne en 32 bits (soit 4 octets). Il ne sait gérer que des zéros et des uns. Alors pas besoin de lui balancer une super syntaxe magistrale, il n'y comprendra rien. En revanche, il connaît des instructions de base (qu'on a pu voir au paragraphe précédent).
Le processeur (CPU) est en attente. Pour savoir ce qu'il doit exécuter, il faut le commander. Pour ce faire, on lui envoie un nombre sur 32 bits. Par exemple en lui envoyant la clé 40h (qui vaut 64 en décimal), vous exécutez la commande :
INC eax
Pour schématiser, il y a la mémoire pour les nombres entiers, pour les nombres flottants, pour les chaînes de caractère... Bref, EAX correspond aux nombres entiers. En faisant INC eax, vous incrémentez le contenu d'une variable qui aurait été déclarée en integer. La capture suivante montre un programme qui n'a aucun sens en soi, mais vous allez voir.
Delphi a décodé pour nous l'état de notre travail juste avant que l'instruction en rouge ait été exécuté. Vous imaginez bien la puissance de Delphi sur cet outil de débuggage techniquement ultra-compliqué vis-à-vis des novices :
assm.dpr.5: inc EAX 00407670 40 inc eax
[Nom du fichier].[Ligne] : [Code en Delphi] [Adresse] [Instruction binaire en hexadécimal envoyée au processeur] [Equivalent en assembleur]
C'est là que j'ai vu que l'instruction binaire 40h correspond à INC eax...
Évidemment, les correspondances entre l'électricité et les commandes ASM nécessitent d'avoir une norme qui dit que çà correspond à cela et pas à ceci. On a donc fixé les normes de Intel (c'était à l'époque des premiers processeurs), et ça a donné lieu à l'assembleur dit pour Intel x86. DW porte bien son nom : Dependency Walker for Win32 (Intel x86). Si une application a été compilée dans une autre norme, alors elle sera quand même exécutée en Intel x86, ce qui peut créer de graves instabilités. Par exemple, Borland C++Builder (qui fait du C++ mais également du Pascal) propose des modes de compilation pour différents processeurs.
A chaque instruction correspond une adresse (ex: 00407670h). Eh bien, l'adresse de la première instruction codant pour la fonction ABCDEF détermine ce qu'on appelle le point d'entrée pour la fonction considérée. De même, le BEGIN général du fichier DPR détermine le point d'entrée de l'application.
Dans un programme, quand une exception se produit, vous pouvez lire "Erreur à l'adresse xxxx:xxxx". C'est la même chose que ci-dessus... Hélas, une fois l'application déployée, retrouver la commande Pascal à l'origine de l'adresse xxxx:xxxx est une autre affaire. Delphi le sait facilement, car c'est lui qui a créé l'application, donc il a des historiques dans les fichiers DCU qui l'aident en ce sens.
Pour vous convaincre de tout ce blabla, on va faire un petit travail pratique. On va créer une DLL sous forme d'EXE... En faisant ainsi, ça sera plus facile de debugger. Créez donc le fichier DPR suivant :
program assm; uses SysUtils; procedure Fonction; asm inc EAX end; exports Fonction; begin Fonction; end.
N'oubliez pas de mettre un point d'arrêt sur INC eax. Il suffit de cliquer dans la marge au niveau de la même ligne et ça devient rouge (par défaut). Lancez l'application. Delphi stoppe et vous voyez à l'écran ceci :
assm.dpr.5: inc EAX 00407608 40 inc eax
Si la fenêtre ne s'affiche pas, cliquez sur >"Voir" >"Fenêtre CPU".
Notez le numéro, et regardez ce que renvoie DW :
L'Entry Point est situé à l'adresse 00007608h selon DW, et à 00407608h pour Delphi. Je n'explique pas pourquoi un chiffre 4 apparaît, car je ne le sais pas, mais je crois que cette démonstration est très convaincante.
Pour aller encore plus loin, selon la fenêtre CPU, notre procédure FONCTION a été traduite ainsi :
inc EAX ret ;Correspond au END final de la procédure
Le plus étonnant dans l'affaire, c'est la compilation. Ce qui suit se résume seulement à 2 octet : 40 C3
procedure Fonction; asm inc EAX end;
Ce n'est alors pas étonnant de voir des projets C++ de 300 ko devenir des EXE de 20 ko seulement... Ça Delphi ne sait pas faire sans créer de dépendances vis-à-vis des paquets d'exécution BPL.
On n'a pas encore défini ce qu'est l'Ordinal. En tout cas, ce n'est pas important. Disons que c'est l'index de la fonction dans la table d'exportation. Voyez cette DLL complètement creuse :
library DLL; procedure FuncOne; begin end; procedure FuncTwo; begin end; exports FuncOne index 1; exports FuncTwo index 2; begin end.
Amusez vous à changer les INDEX et voyez l'effet avec DW. Delphi effectuera des corrections en cas de litige. C'est comme si vous ordonniez l'ordre des fonctions dans la table. A première vue, ça paraît gadget, mais à deuxième vue, je n'ai rien à répondre.
En tout cas, si vous mettez ce qui suit, vous aurez un N/A dans la table en position 2.
exports FuncOne index 1; exports FuncTwo index 3;
Peut-être ces index servent-ils à appeler des fonctions juste en connaissant leur position dans la table et non leur nom ?!?
DW utilise des pictogrammes pour afficher les dépendances.
Tous les fichiers sont trouvés, et on retrouve les liens apparentés déjà analysés.
php_java.dll est une extension pour PHP. En réalité, php4ts.dll est situé dans le répertoire parent du dossier où se trouve php_java.dll. La recherche naturelle et automatique est :
le dossier en cours c:\windows\system32\ c:\windows\system\ c:\windows\ c:\ |
puis dans les dossiers spécifiés dans les variables environnementales (comme ça se faisait avec AUTOEXEC.BAT)
Si le fichier n'est pas trouvé, on voit un logo jaune. Cela ne signifie pas que l'application ou la DLL ne fonctionne pas...
Soit vous avez ouvert un fichier DOS 16 bits, soit une application GUI pour Win 3.1, soit un fichier qui n'est pas exécutable. Bref, DW ne supporte que le 32 bits. Pour avoir des informations sur ces fichiers 16 bits, utilisez QuickView sous Windows 98. DW affiche dans le cadre N°4 la raison du pictogramme rouge.
Il paraîtrait que Half-Life ait un problème avec DirectX (en réalité, c'est faux, c'est un trucage pour montrer le pictogramme). Vous pouvez être sûr que l'application ne se lancera pas. Vous avez de même le logo en cas d'erreur reliée.
C'est l'écran le plus dangereux que vous puissiez obtenir (cette capture est une fois de plus truquée) :
Cela signifie que l'application a été modifiée après sa compilation. Ses tables d'importations et/ou d'exportations ont été flouées, ce qui peut entraîner des instabilités si un logiciel tente d'accéder au fichier. Si vous voyez ça, jettez immédiatement l'application... !!! La personne qui est derrière a des intentions malveillantes. De plus, DW ne recherchant que ces tables, rien ne dit que d'autres sections n'ont pas elles aussi été modifiées.
Beaucoup de logiciels gèrent ce qu'on appelle des plugins. Qu'est-ce que c'est ?
Ce sont tout simplement des DLL, mais dont les appels ne sont pas consignés dans les tables. Cela donne une flexibilité et si la DLL est manquante, le démarrage de l'application ne sera pas bloqué.
Alors comment fait-on pour faire des plugins ? Regardez donc les exemples suivants qui sont tous les deux des fichiers DPR.
program Serveur; uses Windows; var CallProc : procedure(i:integer); pascal; DHandle : integer; begin DHandle:=LoadLibrary('client.dll'); //on recherchera dans le dossier en cours en premier try if DHandle<>0 then begin @CallProc:=GetProcAddress(DHandle, 'DllRunMain'); CallProc(49); end; finally FreeLibrary(DHandle); end; end.
library Client; uses Dialogs, SysUtils; procedure DllRunMain(Value:integer); pascal; begin ShowMessage('Vous avez sélectionné : '+IntToStr(Value)); end; exports DllRunMain; begin end.
Compilez ces deux projets avec F9.
Note importante : le projet CLIENT doit être obligatoirement une DLL grâce au mot clé LIBRARY. Cela se justifie par le fait que Delphi insère un marqueur caractéristique. Vous ne devez pas utiliser le mot clé PROGRAM.
Analyser SERVEUR.EXE avec DW et vous ne voyez aucun appel statique...
... et pourtant vous utilisez bien CLIENT.DLL.
Lancez l'exécutable et observez le beau message qu'il s'affiche.
C'est pas magique tout ça ?
Alors comment savoir si un logiciel utilise telle ou telle DLL ? Eh bien, il n'y a pas vraiment de solution miracle sur l'instant.
Je dois rajouter une chose vis-à-vis de l'ouverture et de la fermeture de la DLL. Si vous oubliez de libérer la DLL avec FreeLibrary, sachez que Kernel32 le fera à votre place lorsque l'application appelante se coupe. Attendez 3 secondes... MAIS que cela ne vous empêche pas de travailler proprement. Vous devez utiliser un bloc
TRY...FINALLY...END
pour vous assurer que la mémoire a été libérée.
Lorsque vous exécutez SERVEUR.EXE, le curseur système de lecture de l'application se déplace de bas en haut. Quand il arrive sur
CallProc(49)
, il change de module et passe au fichier DLL à l'Entry Point. Et donc, tant que vous n'avez pas cliqué sur OK, le fonctionnement de l'application SERVEUR.EXE est bloqué. Heureusement, sinon la DLL serait libérée au moment où elle fait son boulot.
On remarquera quand même que ces appels ne sont pas aussi dynamiques que prévu. On a déclaré une variable de
type procedure(i:integer); pascal;
. A ma connaissance, il n'est pas possible de moduler dynamiquement les types pour s'adapter à telle ou telle configuration. Ainsi, si une DLL a une syntaxe spéciale que l'application ne connait pas, alors elle ne pourra pas être lue. De toute façon, lorsqu'on fait des plugins, c'est pour un logiciel précis et pas pour piquer les DLL des autres.
Autre chose encore et ce sera tout. Pour ces DLL, il existe des conventions d'appels. Pour afficher la page de l'aide, écrivez dans Delphi "stdcall", cliquez une fois sur ce mot et appuyez sur F1. Vous avez alors la feuille "Convention d'appel" qui résume register, pascal, cdecl, stdcall et safecall. Ces conventions sont importantes. Ainsi, si ici j'ai utilisé PASCAL, avec les appels sur Kernel32, il faudra utiliser STDCALL.
TDump est un utilitaire caché livré avec Delphi. Il propose des informations détaillées très précises, et plus exhaustives que Dependency Walker. C'est un logiciel 32 bits utilisé en ligne de commande. Il vous suffit donc de créer un fichier BAT et d'y mettre dedans une commande du type :
"c:\program files\borland\delphi\bin\tdump.exe" "c:\dossier\mon_application.exe" "c:\dossier\dumped_file.txt"
Le fichier généré est plus ou moins gros. Il suffira de l'ouvrir avec WordPad. Vous y trouverez les en-têtes d'exécution, les tables d'importation, les ressources... vraiment tout en détail.
Avec ce tuto, vous devriez être prêt à entamer des séries de debuggage avec Delphi... Ca devrait paraître moins sombre.
Tanguy ALTERT,
http://altert.family.free.fr/