[delphi] dll, plugins et utilisation de tdump et dependency walker for win32 (intel x86)

[delphi] dll, plugins et utilisation de tdump et dependency walker for win32 (intel x86)

Vous allez tout comprendre sur les DLL... :)

Introduction

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 :

  • de voir si tous les appels à DLL sont corrects
  • de savoir si un EXE sera probablement exécutable sur une autre version de Windows sans même lancer l'application
  • de dénicher les fonctions insolites de Windows non référencées dans la VCL de Delphi
  • de savoir si une application a des rapports étroits avec le multimédia, les réseaux, le graphisme, les imprimantes...
  • de trouver les bugs donc
  • de comparer les versions
  • etc...

Accrochez-vous bien...

Note importante avant toute utilisation

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.

Interface du 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.

PARTIE N°1

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.

PARTIE N°2

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".

PARTIE N°3

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".

PARTIE N°4

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.

A quoi ressemble l'assembleur ?

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...

Notion de point d'entée

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.

Index de table

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 ?!?

Affichage des informations

DW utilise des pictogrammes pour afficher les dépendances.

FUNCTIONNAL

Tous les fichiers sont trouvés, et on retrouve les liens apparentés déjà analysés.

FILE NOT FOUND

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...

32 BITS REQUIRED

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.

FAILURE

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.

FILE IS MODIFIED

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.

Appels dynamiques sur des DLL

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

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.

Conclusion

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/

A voir également
Ce document intitulé « [delphi] dll, plugins et utilisation de tdump et dependency walker for win32 (intel x86) » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous