l'assembleur  [livre #4061]

par  pierre maurette



Dll
(Windows)

Bien que le principe soit universel, les DLL (Dynamic-Link Libraries) sont des objets typiquement Windows, toutes versions jusqu'à maintenant. Ce sont, nous l'avons vu, des bibliothèques de fonctions et/ou de ressources qui peuvent accompagner un programme utilisateur pour, par exemple, partager des fonctions entre plusieurs modules. Par-dessus tout, c'est dans des DLL, comme user32.dll ou gdi32.dll , que se trouve une partie du cœur de Windows. Nous avons appris à les appeler depuis l'assembleur, par l'intermédiaire de fichiers  .lib et si possible  .inc , d'origines diverses.

Rappelons quelques points fondamentaux concernant les DLL, tout en sachant qu'il s'agit d'un sujet Windows pour lequel vous devez effectuer votre propre travail de documentation :

  Une DLL peut être chargée de deux façons différentes :

  Soit de façon implicite , automatiquement par le loader de Windows. Dans ce cas, elle devra porter l'extension  .dll . De plus, le programme utilisateur devra avoir été lié à une librairie d'importation  .lib , construite à partir de la DLL ou alors contenir dans sa section IMPORTS toutes les fonctions utilisées.

  Soit de façon explicite , par utilisation des fonctions de Windows LoadLibrary , GetProcAddress et FreeLibrary .

  Dans le cas d'un chargement implicite ou d'un chargement explicite si le chemin complet n'est pas spécifié, Windows effectuera la recherche dans cet ordre :

  Le dossier depuis lequel l'application est chargée.

  Le dossier courant.

  Sous Windows 9X : Le dossier système Windows, C:\WINDOWS\SYSTEM par exemple.

  Sous Windows NT : Le dossier système 32 bits Windows, D:\WINNT\SYSTEM32 par exemple.

  Sous Windows NT : Le dossier système 16 bits Windows, D:\WINNT\SYSTEM par exemple.

  Le dossier Windows, D:\WINNT ou C:\WINDOWS par exemple.

  Les dossiers listés dans la variable d'environnement PATH .

  Windows charge la DLL en mémoire physique à la demande du premier processus utilisateur et la décharge quand le dernier se ferme ou invoque FreeLibrary . Une seule copie du code des fonctions est physiquement présente en mémoire, c'est par le gestionnaire de mémoire virtuelle que cette copie peut être vue à différentes adresses, par les processus clients. La DLL est dite mappée dans l'espace mémoire de ces processus.

Sauf cas particuliers, où la présence de la DLL dans l'espace de plusieurs processus est utile, comme par exemple l'implémentation d'un hook clavier, les DLL ne présentent pas grand intérêt pour un programme de petite ou moyenne taille en assembleur : autant lier ses librairies de fonctions de façon statique.

Leur usage pourrait éventuellement se justifier dans le cadre d'un ensemble de programmes travaillant dans le même domaine et partageant par leur intermédiaire un bouquet de fonctions. Ces programmes clients pourraient sans problème être écrits à l'aide de plusieurs langages différents.

Les DLL sont parfois indispensables pour résoudre certains problèmes, par leur capacité d'être mappées dans l'espace de tous les processus. C'est ce que nous verrons à propos de hooks clavier.

L'appel de fonctions hébergées dans des DLL par un langage évolué est d'ailleurs une possibilité intéressante, particulièrement dans le cas de Visual Basic. Il semble effectivement que ce soit la seule solution pour utiliser du code assembleur dans une application VB.

Nous allons donc écrire une DLL parfaitement basique, puis tester l'appel à partir d'un exécutable écrit en assembleur, puis de modules en VB 6 et en VB 7.

13.1 Écrire une DLL

Pour les programmes assembleur de ce paragraphe, nous nous plaçons dans le même contexte qu'au chapitre sur la programmation Windows, auquel vous devez vous reporter. C’est-à-dire que nous travaillons en 32 bits flat et conseillons fortement le téléchargement de l'ensemble MASM32. En revanche, nous n'utilisons rien de spécifique à ce package. Voyez, sur le CD-Rom, les différents fichiers, Lise & Moi.txt compris, dans le dossier DLL .

Il est clair que dans une DLL doivent figurer les blocs de code source correspondant aux diverses fonctions exposées. Mais avant, il faut penser à l'initialisation de la librairie. C'est toujours par Windows, même quand le programme client utilise la fonction LoadLibrary() , qu'est chargée la DLL. D'une certaine façon, il l'enregistre, pour ne pas la charger deux fois si un autre programme fait la même demande.

Quand Windows charge la DLL, comme dans un programme normal, il lance la DLL non pas au niveau d'une fonction exposée, mais au point d'entrée, encore une fois comme tout exécutable Windows. Le nom de la procédure d'entrée est libre, nous avons choisi DllMain() . Mais cette routine ne sera pas appelée qu'une seule fois. Elle le sera également à la libération de la DLL. Mais surtout, elle sera appelée à chaque fois qu'un nouveau processus deviendra client de la DLL. Notez que chacun de ses appels se fera dans le contexte mémoire du processus client . Si la DLL initialise des variables d'instance, chaque processus client aura ainsi son propre jeu de variables.

De plus, DllMain() est appelée dans le contexte de ce thread à chaque fois qu'un nouveau thread est créé par le processus, mais ce point peut être négligé en première approche.

Voyons le début du code :

.386
.model flat, stdcall
option casemap:none
 
 
.NOLIST
include c:\masm32\include\windows.inc
include c:\masm32\include\kernel32.inc
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
 
.data
MsgBoxTexte      db "      Voila, c'est tout ... ",0
MsgBoxAttach     db "        J'arrive  ...    ",0
MsgBoxDetach     db "        Je trisse ...    ",0
MsgTitre         db "Hello, je suis l'Adèle Elle Elle",0
 
.code
DllMain proc hInstDLL:HINSTANCE, reason:DWORD, bidon:DWORD
    mov eax, reason
    .IF(reason == DLL_PROCESS_ATTACH)
        invoke MessageBoxA, NULL, addr MsgBoxAttach, addr MsgTitre, MB_OK
    .ELSEIF(eax == DLL_PROCESS_DETACH)
        invoke MessageBoxA, NULL, addr MsgBoxDetach, addr MsgTitre, MB_OK
    .ENDIF
    mov  eax,TRUE
    ret
DllMain Endp

La fonction DllMain() reçoit trois arguments : le premier n'est autre qu'un handle sur la DLL, que demandent certaines fonctions, comme GetModuleFileName() . Le troisième n'a pas d'affectation dans les versions actuelles de Windows. Le second, reason , est le plus intéressant ici. Il peut prendre une valeur parmi quatre :

  DLL_PROCESS_ATTACH  ;

  DLL_PROCESS_DETACH  ;

  DLL_THREAD_ATTACH  ;

  DLL_THREAD_DETACH  ;

Le nom de ces valeurs de paramètres est suffisamment parlant pour se passer de commentaire.

Le code de DllMain() peut fort bien être réduit à rien, nous avons mis des MessageBoxA() pour voir ce qui se passe.

Si la DLL accepte le chargement, elle renvoie TRUE (valeur non nulle). Si elle refuse, suite par exemple à un échec dans une allocation de mémoire, elle renvoie FALSE , ou 0 .

Ensuite, il suffit de coder les fonctions. Par exemple :

Test1DLL proc
    invoke MessageBoxA, NULL, addr MsgBoxTexte, addr MsgTitre, MB_OK
    ret
Test1DLL endp

Il nous reste à créer le fichier sk_dll.def , qui tient ici compte de trois fonctions exposées par la DLL :

LIBRARY     SK_DLL
EXPORTS     Test1DLL
            Test2DLL
            Test3DLL

Le fichier build.bat sur le CD-Rom, adapté à votre configuration, vous fournit la syntaxe d'appel de l'assembleur et du lieur :

C:\masm32\bin\ml /c /coff /Cp /Fl %1.asm
C:\masm32\bin\Link /DLL /SUBSYSTEM:WINDOWS /DEF:%1.DEF %1.obj

Voilà, vous obtenez à la fois le fichier  .dll et le fichier de librairie d'importation en  .lib . Le plus simple, dans un premier temps, est de copier la DLL et éventuellement la librairie d'importation (si vous souhaitez l'utiliser) dans le dossier de l'application de test.

Sur un vrai projet, il faudra mettre en place des stratégies plus efficaces pour tester et déboguer confortablement à la fois le programme client et la DLL.

13.2 Appeler une DLL en assembleur

Appeler la DLL directement en assembleur ne pose aucun problème particulier. S'il y a des paramètres à passer ou une valeur de retour à récupérer, tout se passe comme pour une fonction normale.

Méthode explicite

La seule chose un peu particulière, c'est qu'il est important de gérer les erreurs, en signalant, ce qui n'est pas fait ici, le nom de la DLL et/ou de la fonction qui aurait été impossible à charger.

En oubliant l'en-tête, le code est le suivant :

.data 
NomDLL         db "sk_dll.dll",0 
NomFonction    db "Test1DLL",0 
ErreurDLL      db "Impossible de charger la DLL",0 
Titre          db "Test DLL",0 
ErreurFonction db "Impossible de trouver la fonction",0 
 
.data? 
hDLL          dd ? 
TestDLLAddr   dd ?
 
.code 
start: 
 
invoke LoadLibrary,addr NomDLL
 
 .IF eax==FALSE 
    invoke MessageBox,NULL, addr ErreurDLL, addr Titre, MB_OK 
 .ELSE 
    mov hDLL,eax 
    
invoke GetProcAddress, hDLL, addr NomFonction
 
    .IF eax==FALSE 
       invoke MessageBox,NULL, addr ErreurFonction, addr Titre, MB_OK 
    .ELSE 
       
mov TestDLLAddr,eax
 
       
call [TestDLLAddr]
 
    .ENDIF 
    invoke FreeLibrary,hDLL 
 .ENDIF 
 invoke ExitProcess,NULL 
end start

Il n'y a que trois phases importantes, qui sont en gras dans le code. Le call [TestDLLAddr] avec un offset récupéré par GetProcessAddress() montre bien que la fonction Test1DLL est bien dans l'espace mémoire du programme.

Méthode implicite

Il suffit de copier le fichier de la librairie d'importation, ici sk_dll.lib , dans le dossier de l'application. Le code est ensuite extrêmement simple :

includelib sk_dll.lib
 
Test1DLL PROTO
 
.data
NomDLL         db "sk_dll.dll",0
ErreurDLL      db "Impossible de charger la DLL",0
Titre          db "Test DLL",0
ErreurFonction db "Impossible de trouver la fonction",0
 
.data?
hDLL          dd ?
TestDLLAddr   dd ?
 
.code
start:
 invoke Test1DLL
 invoke ExitProcess,NULL
end start

Quand les prototypes de fonctions deviendront plus compliqués, avec valeur de retour et paramètres, il sera préférable d'accompagner le  .lib d'un fichier d'en-tête, sk_dll.inc dans notre cas. En fait, les trois fichiers  .dll , .lib et .inc doivent être fabriqués en même temps. Le dernier nommé est un emplacement approprié pour écrire en commentaire une petite aide sur les fonctions proposées.

13.3 Appeler une DLL à partir de Visual Basic

Nous avons testé l'appel de trois procédures et fonctions de la DLL à partir d'un embryon de programme en Visual Basic. Nous avons voulu tester les versions 6 et 7 (.NET) de ce langage. Pour être tout à fait franc, VB n'étant pas du quotidien de l'auteur, la version sous VB 7 a simplement été obtenue par mise à niveau à partir de la version VB 6. Ce qui a au moins l'avantage de montrer que cette manipulation peut fonctionner.

L'utilisation de DLL depuis VB est simple. Le tout est de respecter ce que nous offre (nous impose ?) ce langage en termes de passage de paramètres et de retour de valeur.

La première procédure, Test1DLL , que nous connaissons déjà, ne renvoie rien (c'est une procédure) et ne prend pas d'argument. Nous verrons que son utilisation est très simple.

 

La deuxième Test2DLL est toujours une procédure, donc ne renvoie toujours rien. Elle prend deux entiers de type Long param1 et param2 en paramètres et attend que param1 prenne la valeur (param1/4)+param2 (pourquoi faire simple...). VB va donc envoyer param1 ByRef . C'est donc un pointeur que va recevoir Test2DLL . La suite coule de source :

Test2DLL proc
; Test2DLL(sdword* pparam1, sdword param2)
; pparam1 pointe vers param1 (*pparam1 = param1)
; Renvoie dans *pparam1 la valeur (param1 / 4) + param2
    enter   0, 0
    mov eax, dword  ptr[ebp+8]
    mov ecx, sdword ptr[eax]
    shr ecx, 2
    add ecx, sdword ptr[ebp+12]
    mov sdword ptr[eax], ecx
    leave
    retn 8
Test2DLL endp

Sans que ce soit indispensable ici, nous avons utilisé ENTER et LEAVE , ce qui est courant au contact d'un langage évolué. Lire ou relire à ce sujet le chapitre Pile, cadres de pile et sous-programmes .

La fonction récupère le pointeur en EBP+8 (param1) , le dé-référence dans ECX et, en fin de calcul, dépose le résultat à l'adresse pointée par EBP+8 .

 

La troisième Test3DLL est une fonction. Elle prend les deux paramètres de type Long par valeur et renvoie le même résultat que précédemment. Pour un Long, le retour se fait dans EAX. D'où le code coté assembleur, plus simple encore que le précédent :

Test3DLL proc
; sdword Test2DLL(sdword param1, sdword param2)
; Renvoie dans EAX la valeur (param1 / 4) + param2
    enter   0, 0
    mov ecx, sdword ptr[ebp+8]
    shr ecx, 2
    add ecx, sdword ptr[ebp+12]
    mov eax, ecx
    leave
    retn 8
Test3DLL endp

Notez qu'utiliser ECX avant de faire mov eax, ecx est idiot. C'est parfois comme ainsi quand nous dérivons une version de la précédente !

Maintenant que notre DLL est écrite, assemblée et liée, passons à VB. Seul le fichier  .dll sera utile.

Copiez-le dans le dossier de l'exécutable VB (un dossier BIN par défaut, dans le cas de VB 7).

Faites un semblant de projet comprenant trois boutons et une TextBox (ou un Label).

Fiche de l'application
figure 13.01 Fiche de l'application

Déclarez d'abord les trois procédures/fonctions :

Option Explicit
Private Declare Sub Test1DLL Lib "SK_DLL" ()
Private Declare Sub Test2DLL Lib "SK_DLL" (ByRef param1 As Long, ByVal param2 As Long)
Private Declare Function Test3DLL Lib "SK_DLL" (ByVal param1 As Long, ByVal param2 As Long) As Long

Ensuite, appelez-les tout à fait normalement dans les traitements OnClick de chaque bouton :

Private Sub BtnDLL1_Click()
  Test1DLL
End Sub
 
Private Sub BtnDLL2_Click()
  Dim x As Long, y As Long
  x = 1000
  y = 46
  Test2DLL x, y
  TextBox.Text = x
End Sub
 
Private Sub BtnDLL3_Click()
  Dim x As Long, y As Long
  x = 500
  y = 31
  TextBox.Text = Test3DLL(x, y)
End Sub

Les valeurs entières sont renvoyées sous VB dans EAX, plus exactement dans AL, AX ou EAX selon la taille de l'entier. Les valeurs virgule flottante, Single comme Double, le sont dans le registre FPU ST0.

Pour les autres types, UDT particulièrement, VB les renvoie dans le couple EDX:EAX , s'il peut. Il semble plus pratique et plus fiable, sorti des types de données simples, de passer ByRef une variable des données plus complexes comme justement ces UDT et de récupérer ainsi le résultat.

Maintenant que vous êtes en possession de cet excellent exécutable, il peut être instructif de faire quelques manipulations : lancez-le plusieurs fois et cliquez sur un peu tous les boutons, dans le but de constater le passage dans MainDll() deux fois pour chaque instance lancée, au lancement et à la fermeture.

DLL_1 sous VB 7
figure 13.02 DLL_1 sous VB 7

13.4 HOOKS

Pour clore ce chapitre sur les DLL, voire la programmation Windows, nous allons étudier une petite application. Le but en est de modifier le comportement du clavier, en changeant le fonctionnement des touches (généralement peu utilisées)  Impr écran  ,  Arrêt défil  et  Pause  .

Ce sont les trois touches situées en haut à droite des claviers standard. À titre d'exemple, elles génèreront respectivement les séquences  {|}   [|]   ->| . Le signe  | représente la position du curseur à l'issue de la frappe.

Modifier le code d'une touche dans la WndProc() d'un programme ne semble pas très complexe. Mais ce n'est pas ce que nous souhaitons : nous voulons que ce comportement soit général, c’est-à-dire qu'il modifie le comportement du clavier dans toutes les applications tournant sur la machine. Pour cela, il va nous falloir installer un hook sur le clavier.

Un hook , en bon français crochet, est un point dans le système de messages où une application peut placer une procédure pour surveiller et modifier les messages avant qu'ils n'atteignent leur destination, la fenêtre cible.

Un programme, dont ce peut être le seul but, est nécessaire pour installer un hook. Le point clé de ce programme est la fonction SetWindowsHook()  (obsolète) ou SetWindowsHookEx() .

invoke LoadLibrary, addr NomDLL
mov    HookDll, eax
invoke GetProcAddress, HookDll, addr NomFonction
;installation du hook
invoke SetWindowsHookEx, WH_KEYBOARD, eax, HookDll, NULL

  Le premier paramètre est une constante indiquant le type de hook à installer, ici un hook clavier.

  Le quatrième paramètre indique le thread (fil d'exécution, comprendre en gros, processus) associé au hook. Si ce paramètre est nul, tous les threads sont surveillés.

  Le deuxième paramètre est l'adresse de la fonction de hook, chargée de (pré)traiter les messages. Si le hook est associé à un thread externe au programme installant le hook, à fortiori si le paramètre 4 est nul, cette fonction doit être dans une DLL (pour pouvoir être mappée dans le contexte de ce ou ces threads). Le premier paramètre, le type du hook, définit une forme particulière pour la procédure de hook. Comme très souvent sous Windows, il est commode de conserver le nom donné par la documentation à cette procédure, ici KeyboardProc() , bien que ce ne soit pas obligatoire. Certains emploient KeybProc() .

  Le troisième paramètre est un handle sur la DLL contenant la procédure de clavier ou est nul s'il ne s'agit pas d'une DLL.

Il nous faut maintenant nous documenter sur la forme que nous devons donner à KeyboardProc() , toujours dans la documentation de Windows :

LRESULT CALLBACK KeyboardProc(
  int code,       // hook code
  WPARAM wParam,  // virtual-key code
  LPARAM lParam   // keystroke-message information
);

En première approche, il semble que la seule valeur de code qui nous autorise à traiter le message est HC_ACTION . Nous récupérons le code clavier dans WPARAM , et LPARAM ne nous intéresse à priori que par son bit 31 : 0 si la touche est pressée, 1 si elle est relâchée.

Les projets avec DLL sont assez délicats à déboguer, une DLL ne pouvant pas se tester seule. Il est donc important de procéder par étapes successives. La première étape consiste à fabriquer le programme de chargement de la DLL sans chargement de DLL. Cette étape ne devrait poser maintenant aucun problème, il existe généralement un squelette ou une application à nettoyer puis copier-coller remplissant cet office. Nous aurions pu envisager un simple lanceur, qui ferme immédiatement, mais au moins dans un premier temps, il est important de pouvoir décharger la DLL.

Le projet est sur le CD-Rom dans le dossier HOOK . Nous ne donnons pas ce listing intermédiaire, il suffit d'enlever dans celui de hook.asm les références à la DLL dans WinMain() et dans le traitement du message WM_DESTROY . C'est le fichier hook1.asm du CD-Rom. Une fois assemblé et lié, le programme démarre dans la barre des tâches. En cliquant sur l’icône, il se développe.

MonHook
figure 13.03 MonHook

Ce programme relève du Windows standard. Nous n'avons pas fait de routine spécialisée pour l'initialisation, nous sommes dans un cas ou l'appel à UpdateWindow() n'est pas indispensable (mais ne serait pas gênant). Le traitement de WM_PAINT écrit un simple message centré sur la zone client. À vous de consulter la documentation sur les quelques fonctions GDI utilisées.

Une fois ce programme testé, nous pouvons ajouter les instructions de chargement et de destruction de la DLL, en sachant que nous aurons, dans un premier temps, un message d'erreur dû à l'absence de cette DLL. Ce qui donne le listing :

.386
.model flat, stdcall
option casemap:none
 
 
.NOLIST
include C:\MASM32\INCLUDE\windows.inc
.LIST
include C:\MASM32\INCLUDE\gdi32.inc
include C:\MASM32\INCLUDE\user32.inc
include C:\MASM32\INCLUDE\kernel32.inc
include C:\MASM32\INCLUDE\Comctl32.inc
include C:\MASM32\INCLUDE\comdlg32.inc
 
;     bibliothèques d'importation
includelib C:\MASM32\LIB\gdi32.lib
includelib C:\MASM32\LIB\user32.lib
includelib C:\MASM32\LIB\kernel32.lib
includelib C:\MASM32\LIB\Comctl32.lib
includelib C:\MASM32\LIB\comdlg32.lib
 
WinMain    PROTO   STDCALL hInstance:      HINSTANCE,
                           hPrevInstance:  HINSTANCE,
                           pCmdLine:       LPSTR,
                           CmdShow:        DWORD
 
                .data?
;=================================================================
;VARIABLES (ou données) non initialisées
;-----------------------------------------------------------------
hInstance_appli HINSTANCE   ?
HookDll         HINSTANCE   ?
MonHook         HHOOK       ?
hMainWnd        HWND        ?
CommandLine     dd          ?
hdc             HDC         ?
pStruct         PAINTSTRUCT <>
rect            RECT        <>
TailleX         dword       ?
TailleY         dword       ?
;=================================================================
                .const
;=================================================================
;CONSTANTES
;-----------------------------------------------------------------
szClassName     db "KBHook",0
szAppliName     db "Mon Hook",0
NomDLL          db "hookdll.dll",0
NomFonction     db "KeyboardProc",0
ErreurDLL       db "Impossible de charger la DLL",0
lo              db "Bijôr, M. Crochet !",0
 
 
                .code
;=================================================================
; Point d'entrée du programme
;-----------------------------------------------------------------
; BLOC PRÉ-INITIALISATIONS
;-----------------------------------------------------------------
; Rôle du bloc :
;               Pré-initialisations
;               Appel de WinMain
;               Fin du programme
;-----------------------------------------------------------------
Start:
invoke  GetModuleHandle, NULL
mov     hInstance_appli, eax
invoke  GetCommandLine
mov     CommandLine,eax
invoke  WinMain, hInstance_appli, NULL, CommandLine,SW_SHOWDEFAULT
invoke  ExitProcess, eax
;-----------------------------------------------------------------
; Fin du programme
;=================================================================
 
;=================================================================
; Procédure WinMain()
;-----------------------------------------------------------------
; Rôle du bloc :
;               Initialisations
;               Appel de WinMain
;               Fin du programme
;-----------------------------------------------------------------
WinMain PROC    STDCALL, hInstance:HINSTANCE,
                         hPrevInstance:HINSTANCE,
                         pCmdLine:LPSTR,
                         CmdShow:DWORD
 
LOCAL   msg:MSG, wc:WNDCLASSEX
 
mov wc.cbSize, SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW OR CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
 
mov wc.cbClsExtra, NULL
mov wc.cbWndExtra, NULL
 
mov eax, hInstance
mov wc.hInstance, eax
 
invoke  LoadIcon, NULL, IDI_APPLICATION
mov     wc.hIcon, eax
mov     wc.hIconSm, eax
 
invoke  LoadCursor, NULL, IDC_ARROW
mov     wc.hCursor, eax
 
mov wc.hbrBackground, COLOR_BACKGROUND
mov wc.lpszMenuName,  NULL
mov wc.lpszClassName, OFFSET szClassName
 
invoke  RegisterClassEx, ADDR wc
.IF (eax)
    invoke  CreateWindowEx,WS_EX_LEFT or WS_EX_TOPMOST,
                           ADDR szClassName,
                           ADDR szAppliName,
                           WS_OVERLAPPEDWINDOW,
                           CW_USEDEFAULT,
                           CW_USEDEFAULT,
                           400,
                           100,
                           NULL,
                           NULL,
                           hInstance,
                           NULL
    .IF (eax)
        mov hMainWnd,eax
        invoke  ShowWindow, hMainWnd, SW_MINIMIZE
        ;ouverture de la librairie dynamique
        invoke LoadLibrary,addr NomDLL
        .IF eax == FALSE
                invoke MessageBox,NULL, addr ErreurDLL, addr ErreurDLL, MB_OK
                xor eax, eax
                ret
        .ENDIF
        mov     HookDll, eax
 
        invoke GetProcAddress, HookDll, addr NomFonction
        .IF eax == FALSE
                invoke MessageBox,NULL, addr ErreurDLL, addr ErreurDLL, MB_OK
                xor eax, eax
                ret
        .ENDIF
        ;installation du hook
        invoke SetWindowsHookEx, WH_KEYBOARD, eax, HookDll, NULL
        .IF eax == FALSE
                invoke MessageBox,NULL, addr ErreurDLL, addr ErreurDLL, MB_OK
                xor eax, eax
                ret
        .ENDIF
        mov MonHook, eax
    .ENDIF
.ENDIF
 
.WHILE TRUE
    invoke  GetMessage, ADDR msg, NULL, 0, 0
    .BREAK .IF (!eax)
    invoke  TranslateMessage, ADDR msg
    invoke  DispatchMessage,  ADDR msg
.ENDW
mov eax, msg.wParam
ret
WinMain ENDP
;=================================================================
 
 
;=================================================================
; Procédure WndProc()
;-----------------------------------------------------------------
; Rôle du bloc :
;               Boucle d'analyse et de traitement des messages
;
;-----------------------------------------------------------------
WndProc PROC    STDCALL, hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
mov eax, uMsg
;-----------------------------------------------------------------
.IF (eax == WM_PAINT)
    invoke BeginPaint, hWnd, ADDR pStruct
    ;mov    hdc, eax
    invoke GetClientRect, hWnd, ADDR rect
    invoke SetTextAlign, pStruct.hdc, TA_CENTER
    invoke SetBkMode, pStruct.hdc, TRANSPARENT
    invoke GetTextExtentPoint32, pStruct.hdc, ADDR lo, LENGTHOF lo, ADDR TailleX
    shr    rect.right, 1
    mov    ebx, rect.bottom
    sub    ebx, TailleY
    shr    ebx, 1
    invoke TextOut, pStruct.hdc, rect.right, ebx, ADDR lo, LENGTHOF lo - 1
 
    invoke  EndPaint,hWnd, ADDR pStruct
;-----------------------------------------------------------------
.ELSEIF (eax == WM_DESTROY)
 
    ;libération du hook
    invoke UnhookWindowsHookEx, MonHook
 
    ;fermeture de la librairie dynamique
    invoke FreeLibrary, HookDll
 
    invoke  PostQuitMessage, 0
;-----------------------------------------------------------------
.ELSE
    invoke  DefWindowProc, hWnd, uMsg, wParam, lParam
    ret
.ENDIF
xor eax, eax
ret
;=================================================================
WndProc ENDP
 
END Start

Il nous faut maintenant écrire une version minimale de la DLL pour être tranquille quant à son chargement, avant de passer à des choses un peu plus compliquées. Vous pouvez déjà enregistrer un hook nul, réduit à mettre 0 dans EAX et à retourner. Pour confirmer notre lecture de la documentation, nous allons détecter la touche  Arrêt défil  et afficher une MessageBox .

Première version de HookDll
figure 13.04 Première version de HookDll

En général, deux boîtes apparaissent, une lors de l’appui sur la touche, l'autre au relâchement. Remarquez également, et réfléchissez-y au besoin, que bien que MessageBox() soit bloquante (ou modale), une grande quantité de boîtes peuvent apparaître. Windows appelle KeyboardProc() autant de fois que nécessaire. Le code de cette version minimaliste, sans l'en-tête qui est classique, est :

.data
ScrollMess    db "Touche Scrolling",0
 
.code
DllMain proc hInstDLL:HINSTANCE, reason:DWORD, bidon:DWORD
    mov  eax, TRUE
    ret
DllMain Endp
 
 
KeyboardProc proc code:DWORD,wParam:DWORD,lParam:DWORD
.IF code == HC_ACTION
  cmp wParam, VK_SCROLL
  jne @F
  invoke MessageBox, NULL, addr ScrollMess, addr ScrollMess, MB_OK
@@:
  xor eax, eax
  ret
.ELSE
  xor eax, eax
  ret
.ENDIF
KeyboardProc endp
 
END DllMain

Nous pouvons tester également notre interprétation de lParam et de son bit 31. Nous ajoutons donc :

Appui = 10000000000000000000000000000000b
; oui Appui = 80000000h
...
cmp wParam, VK_SCROLL
jne @F
and lParam, Appui
jne @F
...

Ainsi, chaque appui sur la touche ne provoquera l'apparition que d'un seul message.

Bon, assez reculé, il faut s'attaquer aux choses qui font du mal. Pour les réflexions qui suivent, nous considérons que seule la touche  Pause  nous importe pour alléger la rédaction. Notre but n'est pas de bloquer le message, ni même d'en changer la valeur : quand la touche est enfoncée, nous voulons écrire un mot entier, envoyer l'équivalent d'une suite de touches. Nous allons donc devoir envoyer une série de messages à la fenêtre qui les attend.

Envoyer des messages ? PostMessage()  is good for you :

BOOL PostMessage(
  HWND hWnd,      // handle of destination window
  UINT Msg,       // message to post
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);

La fenêtre qui les attend ? Donc, celle qui détient le focus. GetForegroundWindow() is good for you, too :

HWND GetForegroundWindow(VOID)

Mais pas de chance, GetForegroundWindow()  nous renvoie le handle sur une fenêtre mère, sans tenir compte des processus enfants, qui peuvent fort bien attendre le message. Nous ne savons pas quel enfant a le focus. Nous avons déjà eu ce genre de problème, reportez-vous à la fonction InstanceUnique() dans SK_WIN . Bon, EnumChildWindows()  is good for you, but not easy to manipulate.

BOOL EnumChildWindows(
  HWND hWndParent,         // handle to parent window
  WNDENUMPROC lpEnumFunc,  // pointer to callback function
  LPARAM lParam            // application-defined value
);

Pour la fonction, énumérer les processus enfants revient pour chacun d'entre eux à appeler une fonction particulière, pointée par lpEnumFunc , définie par l'utilisateur, donc vous. Cette fonction, qui est comme KeyboardProc() une fonction de CALLBACK , ou rappel, doit être construite sur le modèle de EnumChildProc()  :

BOOL CALLBACK EnumChildProc(
  HWND hwnd,      // handle to child window
  LPARAM lParam   // application-defined value
);

Muni du handle sur chaque processus enfant, GetFocus()  is good for you, pour contrôler s'il est propriétaire du focus.

Donc, résumons-nous : nous devons écrire une EnumChildProc() qui sera appelée pour chaque fenêtre consommatrice de messages clavier et recevra un seul et unique paramètre défini à notre guise, plus le handle de la fenêtre en question, ce qui est une chance.

Nous allons tester la méthode, dans un premier temps, en générant simplement un caractère ( & ) dans le cas de l'appui sur  Pause  . Toujours dans une perspective de progression dans les difficultés.

D'abord, le message à envoyer :

invoke PostMessage, hwnd, WM_CHAR, '&', 0

WM_CHAR est un message normalement issu de TranslateMessage() , donc prédigéré. Il ne faut par conséquent pas l'envoyer deux fois (appui et relâchement), cela générerait deux caractères. En fait, le lParam à 0 convient tout à fait et nous évite de gaspiller le paramètre de EnumChildProc() pour le transmettre.

Dans la procédure principale du hook :

KeyboardProc proc code:DWORD,wParam:DWORD,lParam:DWORD
.IF   code == HC_ACTION
  cmp wParam, VK_PAUSE
  jne F_FALSE
  and lParam, Appui
  jne F_TRUE
  invoke GetForegroundWindow
  invoke EnumChildWindows, eax, ADDR EnumChildProc, NULL
  jmp F_TRUE
.ELSE
F_FALSE:
  xor eax, eax
  ret
.ENDIF
F_TRUE:
mov eax, 1
ret
KeyboardProc endp

Au label F_FALSE , le message continue sa vie normalement. Au label F_TRUE , il est retiré de la liste des messages. Il est mort. Après les explications précédentes, le fonctionnement est clair. Les messages qui ne sont pas VK_PAUSE continuent leur chemin. S'il s'agit au contraire de VK_PAUSE , soit il s'agit d'un appui, et EnumChildWindows() est appelée, soit d'un relâchement et le message est tué.

Voyons enfin EnumChildProc()  :

EnumChildProc proc hwnd:HWND, Param:LPARAM
 invoke GetFocus
 cmp eax, hwnd
 jne  FinTRUE
 invoke PostMessage, hwnd, WM_CHAR, '&', 0
FinFALSE:
 xor eax, eax
 ret
FinTRUE:
 mov eax, 1
 ret
EnumChildProc Endp

Le fonctionnement est maintenant évident : après avoir vérifié que la fenêtre concernée avait le focus, le message WM_CHAR avec un & est envoyé.

Nous avons zippé hookdll.asm à ce stade, sur le CD-Rom.

Pour finaliser le programme, nous avons plusieurs solutions. Le problème, si cela en est un, c'est que nous désirons traiter trois touches. Les deux voies principales sont les suivantes :

  Fabriquer une procédure EnumChildProc() pour chaque touche traitée. C'est une solution séduisante, chaque procédure se déduisant des autres par copier-coller, seule la séquence de touches à envoyer étant différente et facilement maintenable. De plus, le besoin en paramètres est minimal et, dans d'autres circonstances, il serait possible de transmettre lParam . Ce serait nécessaire si, par exemple, les messages étaient simplement légèrement modifiés.

  Fabriquer une seule EnumChildProc() , en passant en paramètre wParam , ou un code identifiant la touche à remplacer. Le problème est que pour passer ce paramètre plus éventuellement lParam , il faudrait ruser, en transmettant un pointeur sur un semblant de structure contenant les deux paramètres.

En assembleur, le premier choix semble plus tentant. Ce serait plutôt l'inverse en C, pour des raisons de facilité d'écriture des choix multiples et des structures, mais cette opinion est discutable.

De plus, la touche  Impr écran  a un comportement particulier : elle ne génère de message Windows qu'au relâchement. Pour traiter le problème, nous avons décidé de transmettre lParam aux trois procédures EnumChildProc???() et de traiter appui ou relâchement touche par touche.

Ce qui nous donne pour KeyboardProc()  :

KeyboardProc proc code:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ADDRESSE_PROC: DWORD
.IF   code != HC_ACTION
   jmp F_FALSE
.ENDIF
.IF      wParam == VK_SNAPSHOT
    mov ADDRESSE_PROC, OFFSET EnumChildProcSNAPSHOT
.ELSEIF  wParam == VK_SCROLL
    mov ADDRESSE_PROC, OFFSET EnumChildProcSCROLL
.ELSEIF  wParam == VK_PAUSE
    mov ADDRESSE_PROC, OFFSET EnumChildProcPAUSE
.ELSE
    jmp F_FALSE
.ENDIF
  invoke GetForegroundWindow
  invoke EnumChildWindows, eax, ADDRESSE_PROC, lParam
F_TRUE:
  mov eax, 1
  ret
F_FALSE:
  ;invoke CallNextHookEx, NULL, code, wParam, lParam
  xor eax, eax
  ret
KeyboardProc endp

Arrivé à ce stade, il n'y a rien de sorcier dans cette partie du code (ni dans le reste d'ailleurs). Nous pouvons imaginer ce que deviendraient ces quelques lignes sans l'utilisation des .IF , .ELSEIF , invoke , etc.

Voyons une des procédures EnumChildProc?????()  :

EnumChildProcSCROLL proc hwnd:HWND, Param:LPARAM
 and Param, Appui
 jne FinTRUE
 invoke GetFocus
 cmp eax, hwnd
 jne  FinTRUE
 invoke PostMessage, hwnd, WM_CHAR,    '[', 0
 invoke PostMessage, hwnd, WM_CHAR,    ']', 0
 invoke PostMessage, hwnd, WM_KEYDOWN, VK_LEFT, 0
 invoke PostMessage, hwnd, WM_KEYUP,   VK_LEFT, 0
 xor eax, eax
 ret
FinTRUE:
 mov eax, 1
 ret
EnumChildProcSCROLL Endp

Remarquez que la simulation d'une frappe sur la touche  Retour Arrière  demande à simuler l'appui et le relâchement. Bon, c'est comme ça...

Voilà, le listing complet et l'exécutable sont sur le CD-Rom. Il est très facile d'adapter ce programme. Il n'y aurait, par exemple, aucun problème à faire générer des URL pour internet ou des codes d'accès, toujours à priori dans le cadre de la navigation internet.

D'un autre côté, il est possible, en lançant furtivement la DLL, de faire discrètement un fichier de log des touches saisies au clavier. Hum hum.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1