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 œuvre.
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 :
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 équivalant à 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 :
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ésente 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 :
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 ce 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 :
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.
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 :
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.
function
MakeProcOfPascalMethod(const
Method: TMethod): Pointer
;
begin
Result := MakeProcOfStdCallMethod(Method);
end
;