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 où 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.
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.
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 :
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.
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 !
; 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 :
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.
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
;