IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Construire une procédure pointant sur une méthode en Delphi


précédentsommairesuivant

IX. Convention d'appel register

IX-A. Les difficultés

Cette fois-ci, plus question de s'en sortir aussi facilement. Et pourtant la convention register est la convention par défaut en Delphi. Il est donc crucial de pouvoir la traiter correctement.

Pour rappel, cette convention d'appel - qui est la convention fastcall de Pascal - utilise dans l'ordre les registres EAX, EDX et ECX pour transmettre les trois premiers paramètres qui peuvent se transmettre dans ces registres. S'il reste des paramètres, ceux-ci sont transmis sur la pile, dans l'ordre de leur déclaration (comme pascal).

Les chaînes courtes, variants, tableaux et record longs, bien que plus grands que 4 octets, sont transmis par adresse, et entrent donc bien dans les registres. Les types flottants, quels qu'ils soient, sont toujours transmis sur la pile, y compris le type Single qui est stocké sur 4 octets. Les types pointeur sur méthode sont stockés sur 8 octets et toujours transmis sur la pile, tandis que les types pointeur sur procédure peuvent passer par un registre, puisqu'ils sont stockés sur 4 octets.

Tous les paramètres var et out, de quel type qu'ils soient, sont toujours transmis par adresse, et peuvent donc être passés par registre. Les paramètres non typés partagent ce comportement.

La valeur de retour d'une fonction fait parfois partie des paramètres également ! Lorsque le type de cette valeur de retour fait partie des types qui se transmettent toujours par adresse (chaînes courtes, variants, tableaux et record longs), c'est l'appelant qui alloue l'espace où stocker la valeur de retour. L'adresse de cet espace constitue alors un paramètre supplémentaire dans l'appel de la fonction (en dernière position).

Le paramètre Self, qu'il soit de type objet ou métaclasse, est toujours un pointeur stocké sur 4 octets, et peut donc être passé par registre. Puisqu'il est le premier, il est toujours passé dans le registre EAX.

Ce qu'il faut, en revanche, c'est enregistrer l'ancienne valeur de EAX dans EDX, l'ancienne valeur de EDX dans ECX, et l'ancienne valeur de ECX sur la pile, et au bon endroit.

MAIS, car il y a un mais, il ne faut surtout pas ajouter quelque chose sur la pile si la méthode n'a pas de paramètre passé sur la pile. Sinon, la pile sera tout à fait corrompue après le retour de la méthode appelée.

Comment savoir s'il faut ou non insérer ECX dans la pile, et l'insérer ? Pour ne pas devoir tout avaler d'un coup, nous allons progressivement prendre en charge des cas de plus en plus complexes.

IX-B. Cas de base : il ne faut pas stocker ECX

Pour commencer, nous allons faire la supposition honteuse qu'il ne faut jamais stocker ECX dans la pile. Quand cela arrive-t-il ? Quand il y a maximum 2 paramètres de la procédure qui sont passés par registre. Que ce soit parce qu'il n'y en a que 2, ou parce que les autres doivent obligatoirement passer sur la pile (flottants et pointeurs de méthode).

Dans ces cas, qu'a besoin de faire CallBackProc ? Simplement de ranger EDX dans ECX, EAX dans EDX, et finalement stocker dans EAX l'adresse de l'objet. Si EDX ou EAX n'étaient déjà pas utilisés avant, il n'y a aucune conséquence : ces trois registres sont volatils.

 
Sélectionnez
MOV     ECX,EDX
MOV     EDX,EAX
MOV     EAX,CallBackObj
JMP     CallBackMethod

La pile n'est alors pas altérée du tout, et tout est très facile.

IX-C. Ranger ECX au sommet da la pile

Dans un certain nombre de cas, ECX doit être rangé au sommet de la pile, de la même façon qu'on y stockait l'adresse de l'objet pour la convention stdcall. Cette situation arrive lorsque le paramètre Delphi que porte ECX est le dernier paramètre déclaré. En effet, ce paramètre doit alors être empilé juste avant l'instruction CALL.

Dans ces cas-là, on utilise une technique similaire à celle utilisée avec stdcall : déplacer l'adresse de retour un élément plus loin dans la pile, et stocker ECX à l'endroit laissé vide.

Cependant, avec stdcall, nous pouvions utiliser le registre EAX comme endroit temporaire où récupérer l'adresse de retour. Ici, nous n'avons pas le droit de perdre le contenu de EAX, puisqu'il s'agit d'un paramètre. Nous nous servons donc d'une instruction bien pratique : XCHG. Cette instruction échange les informations contenues dans ses deux opérandes. Ainsi, XCHG ECX,[ESP] place la valeur de ECX dans [ESP] et celle de [ESP] dans ECX.

 
Sélectionnez
XCHG    ECX,[ESP]
PUSH    ECX
MOV     ECX,EDX
MOV     EDX,EAX
MOV     EAX,CallBackObj
JMP     CallBackMethod

Vous remarquerez que les quatre dernières instructions ici sont les mêmes que les quatre instructions du cas de base. Nous en profiterons plus tard.

IX-D. Ranger ECX plus haut dans la pile

Lorsqu'ECX n'est pas le dernier paramètre déclaré, tous les paramètres qui le suivent sont empilés après lui. Il faut donc insérer ECX devant ceux-ci. Si l'on connaît le nombre de « cases » de pile à déplacer ainsi (la pile croît et décroît toujours par double mot sous Win32), on peut répéter plusieurs fois l'instruction XCHG avec un offset sur ESP.

Si par exemple il y a 2 cases à déplacer (deux entiers par exemple), on aura ceci :

 
Sélectionnez
XCHG    ECX,[ESP+8]
XCHG    ECX,[ESP+4]
XCHG    ECX,[ESP]
PUSH    ECX
MOV     ECX,EDX
MOV     EDX,EAX
MOV     EAX,CallBackObj
JMP     CallBackMethod

Encore une fois, cette suite d'instructions contient entièrement la suite de la section précédente : plus on complexifie, plus la « préparation » est longue.

La seule limitation qui reste est que le nombre de cases à déplacer ainsi n'atteigne pas 64 (256/4). Sinon on devra utiliser les versions longues de XCHG, et de toute façon on aurait trop d'instructions XCHG ! Une pour chaque case !

IX-E. Une première fonction MakeProcOfRegisterMethod

Nous allons déjà construire une première version de MakeProcOfRegisterMethod qui se base sur ce que nous avons déjà trouvé.

Elle a malheureusement besoin, outre le paramètre Method, de deux paramètres UsedRegCount et MoveStackCount, indiquant respectivement le nombre de registres utilisés dans la procédure (0 à 3) et le nombre de cases de pile à déplacer. Notez que MoveStackCount doit être 0 si UsedRegCount < 3.

Nous utilisons la remarque que les préparations s'ajoutent au début pour construire plus facilement la procédure. Combinée avec des jeux de calculs de tailles, on peut faire quelque chose de pas trop lent.

 
Sélectionnez
function MakeShortProcOfRegisterMethod(const Method: TMethod;
  UsedRegCount: Byte; MoveStackCount: Word): Pointer;

const
  MoveStackItem: LongWord = $00244C87; // 874C24 xx   XCHG ECX,[ESP+xx]
  MoveRegisters: array[0..7] of Byte = (
    $87, $0C, $24, // XCHG    ECX,[ESP]
    $51,           // PUSH    ECX
    $8B, $CA,      // MOV     ECX,EDX
    $8B, $D0       // MOV     EDX,EAX
  );

type
  PRegisterRedirector = ^TRegisterRedirector;
  TRegisterRedirector = packed record
    MovEAXObj: Byte;
    ObjAddress: Pointer;
    Jump: TJmpInstruction;
  end;

var
  MoveStackSize, MoveRegSize: Integer;
  InstrPtr: Pointer;
  I: Cardinal;
begin
  Assert((MoveStackCount = 0) or (UsedRegCount >= 3));

  if UsedRegCount >= 3 then
    UsedRegCount := 4;
  MoveRegSize := 2*UsedRegCount;
  MoveStackSize := 4*MoveStackCount;

  GetMem(Result, MoveStackSize + MoveRegSize + SizeOf(TRegisterRedirector));
  InstrPtr := Result;

  for I := MoveStackCount downto 1 do
  begin
    // I shl 26 => I*4 in the most significant byte (kind of $ I*4 00 00 00)
    PLongWord(InstrPtr)^ := (I shl 26) or MoveStackItem;
    Inc(Integer(InstrPtr), 4);
  end;

  Move(MoveRegisters[SizeOf(MoveRegisters) - MoveRegSize],
    InstrPtr^, MoveRegSize);
  Inc(Integer(InstrPtr), MoveRegSize);

  with PRegisterRedirector(InstrPtr)^ do
  begin
    MovEAXObj := $B8;
    ObjAddress := Method.Data;
    MakeJmp(Jump, Method.Code);
  end;
end;

IX-F. Quand MoveStackCount devient trop grand

Comme il a déjà été fait remarquer, la taille du code croît avec MoveStackCount. Et en plus, trop d'instructions XCHG diminuent les performances.

Lorsque MoveStackCount dépasse un certain seuil, nous allons employer une autre technique, qui s'appuie sur un REP MOVSD pour déplacer en masse les éléments de la pile.

Puisque ESI et EDI ne sont pas volatils, et qu'on ne doit pas perdre l'information enregistrée dans ECX non plus, on doit stocker temporairement le contenu de ces trois registres sur la pile - ce qui implique de prendre en compte qu'ils y sont aussi !

 
Sélectionnez
; First move the stack

PUSH    ESI
PUSH    EDI
PUSH    ECX

MOV     ESI,ESP
PUSH    ECX
MOV     EDI,ESP
MOV     ECX,MoveStackCount+4 ; 4 are ECX, EDI, ESI and ReturnAddress

REP     MOVSD

POP     ECX
POP     EDI
POP     ESI

; Now move registers and jump to the method

MOV     [ESP + 4*(MoveStackCount+1)],ECX ; 1 is ReturnAddress
MOV     ECX,EDX
MOV     EDX,EAX
MOV     EAX,CallBackObj
JMP     CallBackMethod

Ce code est en fait beaucoup plus facile à générer que le premier : il est de taille constante et seules quelques parties sont dépendantes des paramètres. On simplifie énormément en définissant une constante tableau de Byte qui contient le code immuable. Cela donne la routine suivante :

 
Sélectionnez
function MakeLongProcOfRegisterMethod(const Method: TMethod;
  MoveStackCount: Word): Pointer;

type
  PRegisterRedirector = ^TRegisterRedirector;
  TRegisterRedirector = packed record
    Reserved1: array[0..8] of Byte;
    MoveStackCount4: LongWord;
    Reserved2: array[13..20] of Byte;
    FourMoveStackCount1: LongWord;
    Reserved3: array[25..29] of Byte;
    ObjAddress: Pointer;
    Jump: TJmpInstruction;
  end;

const
  Code: array[0..SizeOf(TRegisterRedirector)-1] of Byte = (
    $56,                     // PUSH    ESI
    $57,                     // PUSH    EDI
    $51,                     // PUSH    ECX

    $8B, $F4,                // MOV     ESI,ESP
    $51,                     // PUSH    ECX
    $8B, $FC,                // MOV     EDI,ESP
    $B9, $FF, $FF, $FF, $FF, // MOV     ECX,MoveStackCount+4

    $F3, $A5,                // REP     MOVSD

    $59,                     // POP     ECX
    $5F,                     // POP     EDI
    $5E,                     // POP     ESI

    $89, $8C, $24, $EE, $EE, $EE, $EE,
                             // MOV     [ESP + 4*(MoveStackCount+1)],ECX
    $8B, $CA,                // MOV     ECX,EDX
    $8B, $D0,                // MOV     EDX,EAX
    $B8, $DD, $DD, $DD, $DD, // MOV     EAX,0
    $E9, $CC, $CC, $CC, $CC  // JMP     MethodAddress
  );

begin
  GetMem(Result, SizeOf(Code));
  Move(Code[0], Result^, SizeOf(Code));

  with PRegisterRedirector(Result)^ do
  begin
    MoveStackCount4 := MoveStackCount+4;
    FourMoveStackCount1 := 4 * (MoveStackCount+1);
    ObjAddress := Method.Data;
    MakeJmp(Jump, Method.Code);
  end;
end;

Cette version n'accepte pas de paramètre UsedRegCount : on a en effet décidé de n'utiliser celle-ci que lorsque MoveStackCount dépasse un certain seuil. Et nous avons remarqué que MoveStackCount ne peut être différent de 0 que si UsedRegCount vaut 3.

IX-G. Mettre ensemble les deux techniques

Il ne reste plus qu'à mettre ensemble les deux techniques. Et donc à déterminer un seuil de MoveStackCount. En se basant exclusivement sur la longueur du code généré - ce qui est une approximation « valable » du temps d'exécution -, on trouve que la première technique est utilisable jusqu'à MoveStackCount = 8. Au-dessus de cela, on utilise la seconde version.

Nous avons donc trois procédures finales, dont la dernière est la seule à devoir être utilisée de l'extérieur.

 
Sélectionnez
function MakeShortProcOfRegisterMethod(const Method: TMethod;
  UsedRegCount: Byte; MoveStackCount: Word): Pointer;

const
  MoveStackItem: LongWord = $00244C87; // 874C24 xx   XCHG ECX,[ESP+xx]
  MoveRegisters: array[0..7] of Byte = (
    $87, $0C, $24, // XCHG    ECX,[ESP]
    $51,           // PUSH    ECX
    $8B, $CA,      // MOV     ECX,EDX
    $8B, $D0       // MOV     EDX,EAX
  );

type
  PRegisterRedirector = ^TRegisterRedirector;
  TRegisterRedirector = packed record
    MovEAXObj: Byte;
    ObjAddress: Pointer;
    Jump: TJmpInstruction;
  end;

var
  MoveStackSize, MoveRegSize: Integer;
  InstrPtr: Pointer;
  I: Cardinal;
begin
  if UsedRegCount >= 3 then
    UsedRegCount := 4;
  MoveRegSize := 2*UsedRegCount;
  MoveStackSize := 4*MoveStackCount;

  GetMem(Result, MoveStackSize + MoveRegSize + SizeOf(TRegisterRedirector));
  InstrPtr := Result;

  for I := MoveStackCount downto 1 do
  begin
    // I shl 26 => I*4 in the most significant byte (kind of $ I*4 00 00 00)
    PLongWord(InstrPtr)^ := (I shl 26) or MoveStackItem;
    Inc(Integer(InstrPtr), 4);
  end;

  Move(MoveRegisters[SizeOf(MoveRegisters) - MoveRegSize],
    InstrPtr^, MoveRegSize);
  Inc(Integer(InstrPtr), MoveRegSize);

  with PRegisterRedirector(InstrPtr)^ do
  begin
    MovEAXObj := $B8;
    ObjAddress := Method.Data;
    MakeJmp(Jump, Method.Code);
  end;
end;

function MakeLongProcOfRegisterMethod(const Method: TMethod;
  MoveStackCount: Word): Pointer;

type
  PRegisterRedirector = ^TRegisterRedirector;
  TRegisterRedirector = packed record
    Reserved1: array[0..8] of Byte;
    MoveStackCount4: LongWord;
    Reserved2: array[13..20] of Byte;
    FourMoveStackCount1: LongWord;
    Reserved3: array[25..29] of Byte;
    ObjAddress: Pointer;
    Jump: TJmpInstruction;
  end;

const
  Code: array[0..SizeOf(TRegisterRedirector)-1] of Byte = (
    $56,                     // PUSH    ESI
    $57,                     // PUSH    EDI
    $51,                     // PUSH    ECX

    $8B, $F4,                // MOV     ESI,ESP
    $51,                     // PUSH    ECX
    $8B, $FC,                // MOV     EDI,ESP
    $B9, $FF, $FF, $FF, $FF, // MOV     ECX,MoveStackCount+4

    $F3, $A5,                // REP     MOVSD

    $59,                     // POP     ECX
    $5F,                     // POP     EDI
    $5E,                     // POP     ESI

    $89, $8C, $24, $EE, $EE, $EE, $EE,
                             // MOV     [ESP + 4*(MoveStackCount+1)],ECX
    $8B, $CA,                // MOV     ECX,EDX
    $8B, $D0,                // MOV     EDX,EAX
    $B8, $DD, $DD, $DD, $DD, // MOV     EAX,0
    $E9, $CC, $CC, $CC, $CC  // JMP     MethodAddress
  );

begin
  GetMem(Result, SizeOf(Code));
  Move(Code[0], Result^, SizeOf(Code));

  with PRegisterRedirector(Result)^ do
  begin
    MoveStackCount4 := MoveStackCount+4;
    FourMoveStackCount1 := 4 * (MoveStackCount+1);
    ObjAddress := Method.Data;
    MakeJmp(Jump, Method.Code);
  end;
end;

function MakeProcOfRegisterMethod(const Method: TMethod;
  UsedRegCount: Byte; MoveStackCount: Word = 0): Pointer;
begin
  Assert((MoveStackCount = 0) or (UsedRegCount >= 3));

  if MoveStackCount <= 8 then
    Result := MakeShortProcOfRegisterMethod(
      Method, UsedRegCount, MoveStackCount)
  else
    Result := MakeLongProcOfRegisterMethod(Method, MoveStackCount);
end;

précédentsommairesuivant

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