Construire une procédure pointant sur une méthode


précédentsommairesuivant

VII. Convention d'appel stdcall

VII-A. Le code assembleur

La convention stdcall n'est pas standard pour rien : elle est la plus simple, surtout pour ce que nous voulons faire ici. Nous allons donc commencer par celle-ci pour introduire tous les trucs et astuces que nous mettrons en oeuvre.

En effet, tous les paramètres, sans exception, sont passés sur la pile, et en plus en ordre inverse. Il nous suffit donc d'ajouter notre CallBackObj sur la pile : puisqu'ajouté en dernier sur la pile, il est le premier dans l'ordre des paramètres. Nous n'avons pas besoin de nous soucier des autres paramètres éventuels, car ils n'interfèreront jamais.

D'autre part, c'est la méthode appelée qui supprime de la pile les paramètres (contrairement à cdecl). Or celle-ci est bien au courant qu'un paramètre supplémentaire existe, donc nous n'avons rien à faire (nous verrons plus loin que justement, avec cdecl, cela pose de nombreux soucis).

La seule astuce est qu'il faut insérer le paramètre CallBackObj devant l'adresse de retour, qui est ajoutée automatiquement par CALL et exploitée par RET. Il faut donc sauvegarder la valeur présente actuellement dans la pile et la remettre après avoir stocké CallBackObj. Puisque EAX est volatil, nous avons parfaitement le droit de l'utiliser pour stocker temporairement cette adresse de retour.

Le code assembleur de CallBackProc doit donc être celui-ci :

 
Sélectionnez

POP     EAX
PUSH    CallBackObj
PUSH    EAX
JMP     CallBackMethod
				

VII-B. Code exécutable x86 correspondant

Tout ça c'est très bien, mais on ne connaît toujours pas CallBackObj ni @CallBackMethod. On ne les connaîtra qu'à l'exécution. Donc il faut générer ce code assembleur à l'exécution. Assembleur ? Que dis-je, non ! Il faut générer le code machine équivalent à ce code assembleur.

Un tutoriel très intéressant pour pouvoir générer du code x86 est celui sur le décodage du jeu d'instructions x86/x64, écrit par Neitsa. Ce tutoriel recommande par ailleurs le débogueur OllyDbg, qui a l'énorme avantage de vous permettre de coder des instructions assembleur "sur le vif" et de voir leur encodage x86.

Grâce à cet outil (ou un autre), on obtient facilement le code x86 correspondant à nos 4 instructions assembleur :

 
Sélectionnez

58              POP     EAX
68 xx xx xx xx  PUSH    CallBackObj
50              PUSH    EAX
E9 yy yy yy yy  JMP     CallBackMethod
				

VII-C. Particularités des instructions JMP et CALL

La valeur "yy yy yy yy" dans le code précédent ne représentent pas directement l'adresse de la méthode CallBackMethod. En réalité, elle indique le déplacement à appliquer au registre d'instruction EIP. Et ceci après que l'instruction a été exécutée, donc lorsque EIP pointe sur l'instruction suivante.

Pour connaître l'argument d'une instruction JMP (ou CALL, qui partage son fonctionnement sur ce point), on utilise donc la formule que calcule la fonction JmpArgument suivante :

 
Sélectionnez

function JmpArgument(JmpAddress, JmpDest: Pointer): Integer; inline;
begin
  Result := Integer(JmpDest) - Integer(JmpAddress) - 5;
end;
				

Cette fonction est un cas typique d'utilisation de la directive inline. Celle-ci suggère au compilateur d'optimiser chaque appel à cette fonction en remplaçant cet appel par le contenu de la fonction. Cela rend le code plus rapide, mais a le désavantage de l'agrandir en taille. De plus, ce n'est pas toujours possible. Consultez l'aide de Delphi pour plus d'informations sur que l'on appelle l'inlining.
JmpArgument est bien disposée pour l'inlining car elle opère une formule mathématique. Une unique instruction Result := Expression.
Les connaisseurs de C/C++ reconnaîtront l'adaptation des macros au Pascal.

Comme ce ne sera pas la seule instruction JMP/CALL que nous créerons, épargnons-nous encore un peu de travail avec ces quelques facilités :

 
Sélectionnez

type
  TJmpInstruction = packed record
    OpCode: Byte;      /// OpCode
    Argument: Integer; /// Destination
  end;

procedure MakeJmp(var Instruction; Dest: Pointer);
begin
  TJmpInstruction(Instruction).OpCode := $E9; // OpCode de JMP
  TJmpInstruction(Instruction).Argument := JmpArgument(@Instruction, Dest);
end;

procedure MakeCall(var Instruction; Dest: Pointer);
begin
  TJmpInstruction(Instruction).OpCode := $E8; // OpCode de CALL
  TJmpInstruction(Instruction).Argument := JmpArgument(@Instruction, Dest);
end;
				

J'ai fait ici le choix d'un paramètre Instruction non typé, et de le transtyper en TJmpInstruction à l'intérieur de la procédure. Ceci permet éventuellement d'appeler ces routines avec en paramètre du contenu de n'importe quel type. Ce ne sera pas utile ici mais j'ai eu l'occasion de m'en servir personnellement par ailleurs.
Accessoirement, c'est l'occasion de faire découvrir l'usage de ce type de paramètres.

Au point où nous en sommes (génération d'OpCodes x86), on peut se permettre un peu d'assembleur en ligne pour ces deux routines. Cela n'est utile que pour les versions de Delphi qui ne supportent pas la directive inline. En effet, avec l'inlining de JmpArgument, le code produit par Delphi n'a jamais qu'un MOV de plus que ce que nous présentons ici. En revanche, sans le support de inline, le code est inutilement beaucoup plus long.

 
Sélectionnez

procedure MakeJmp(var Instruction; Dest: Pointer);
asm
        { -> EAX Pointer to a TJmpInstruction record }
        { -> EDX Pointer to destination              }

        MOV     BYTE PTR [EAX],$E9
        SUB     EDX,EAX
        SUB     EDX,5
        MOV     [EAX+1],EDX
end;

procedure MakeCall(var Instruction; Dest: Pointer);
asm
        { -> EAX Pointer to a TJmpInstruction record }
        { -> EDX Pointer to destination              }

        MOV     BYTE PTR [EAX],$E8
        SUB     EDX,EAX
        SUB     EDX,5
        MOV     [EAX+1],EDX
end;
				

VII-D. Générer dynamiquement le code

Nous avons donc tout en main pour créer la première des quatre routines MakeProcOfXXXMethod. Elle est vraiment simple, à présent :

 
Sélectionnez

function MakeProcOfStdCallMethod(const Method: TMethod): Pointer;
type
  PStdCallRedirector = ^TStdCallRedirector;
  TStdCallRedirector = packed record
    PopEAX: Byte;
    PushObj: Byte;
    ObjAddress: Pointer;
    PushEAX: Byte;
    Jump: TJmpInstruction;
  end;
begin
  GetMem(Result, SizeOf(TStdCallRedirector));
  with PStdCallRedirector(Result)^ do
  begin
    PopEAX := $58;
    PushObj := $68;
    ObjAddress := Method.Data;
    PushEAX := $50;
    MakeJmp(Jump, Method.Code);
  end;
end;
				

Vous ne retrouvez pas CallBackObj et CallBackMethod ? Ils se trouvent dans les deux champs de TMethod, respectivement Data et Code.

Pour utiliser cette fonction, il suffit de lui transmettre une méthode transtypée en TMethod, et l'on récupère un pointeur que l'on peut retranstyper en le type procédure du call-back.

N'oubliez pas de libérer le pointeur ainsi obtenu au moyen de FreeMem lorsque vous n'en avez plus besoin.

VIII. Convention d'appel pascal

La différence entre les conventions d'appel stdcall et pascal est subtile mais bien présente. En stdcall, les paramètres sont empilés dans l'ordre inverse de leur déclaration. Ce qui tombe bien, puisqu'ainsi nous avons pu ajouter très facilement le premier paramètre (Self) en dernier, après ce que l'appelant a déjà fait.

En pascal cependant, les paramètres sont empilés dans l'ordre réel de leur déclaration. Ce qui veut dire que la position du premier paramètre dans la pile est dépendante des autres paramètres.

Avant de se lancer dans un déplacement de mémoire de la pile - ce qui semble nécessaire puisqu'il faut insérer le paramètre Self en premier - il est extrêmement intéressant de découvrir ce fait : qu'en réalité, le paramètre Self, en convention pascal, est "déclaré" en dernier ! Et donc se retrouve a être empilé en dernier aussi. Exactement comme en stdcall !

Vous l'aurez compris, nous n'allons pas traîner plus longtemps sur pascal, car la routine MakeProcOfPascalMethod est identique à MakeProcOfStdCallMethod.

 
Sélectionnez

function MakeProcOfPascalMethod(const Method: TMethod): Pointer;
begin
  Result := MakeProcOfStdCallMethod(Method);
end;
			

précédentsommairesuivant

Tutoriels
Les génériques avec Delphi 2009 Win32 (English version) - également disponible en espagnol et en russe
Réaliser un plug-in comportant un composant
Construire une procédure pointant sur une méthode
Création de composants - en 4 parties
Refactoring avec Delphi 2007
Prise en main de Delphi 2005
Analyseurs syntaxiques - Leur fonctionnement par l'exemple
Créer un fichier d'aide HLP
Pourquoi un paramètre const change-t-il mystérieusement de valeur ?
Sources
SJRDUnits - Routines et classes diverses
SJRDComps - Quelques composants
Projet Sepi
Présentation
FAQ Sepi
Programmes
FunLabyrinthe - Jeu de labyrinthe très spécial et très fun
TrickTakingGame - Jeux de cartes à plis en ligne
MultiAgenda - Agenda multi-répertoires
DecodeFormulaires - Décode les formulaires
Excel --> HTML - Convertisseur de tableaux Excel en HTML
AddressLinks - Lie les adresses Internet et e-mail d'un document HTML
Vipion - Tic Tac Toe sur 4x4 cases avec jeu de l'ordinateur
BigCalc - Calculatrice de haut niveau
Espace paroissial Astérion de Watermael-Boitsfort
  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 Sébastien Doeraene. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.