Un CPU x86, c'est à dire la pluspart des processeurs (Intelet AMD) des PCs, comporte des registres qui sont les suivants :
Si vous avez déjà entendu parler de registres de segments en mode réel, tels que CS, DS, ES, FS, GS, SS... et bien oubliez les, car en mode 32 bits protégé, il n'y a pas de segments... ou plutôt, un seul de 4Go (en théorie)...
La pile sert à stocker temporairement les informations nécessaires au fonctionnement du programme : adresse de retour de fonction, variables locales...
La pile est une zone mémoire qui croît vers le bas. Au départ, la pile est au plafond et à chaque fois que l'on empile, le sommet descend de 4 octets du plafond. Lorsque l'on dépile, le sommet remonte de 4octets. Il s'en suit que la mémoire libre est en ESP - 4, ESP - 8...
Et la mémoire utilisée en ESP, ESP + 4, ESP + 8...
Lorsque l'on fait un appel de fonction, l'adresse de l'instruction suivant le CALL (adresse de retour) est poussée sur la pile, puis suivent les paramètres suivant la convention d'appel. La pile ressemble donc à ceci (si passage des paramètres par la pile) :
ESP + 4* n + 4 : Paramètre-n
...
ESP +8 : Paramètre 2
ESP +4 : Paramètre 1
ESP +0 : Adresse de retour
Si l'on utilise EBP, on écrira le code suivant au début de la procédure :
PUSHEBP //on doit sauvegarder EBP avantchangement
MOV EBP,ESP //on garde l'emplacement de pile actuel
SUB ESP, taille_des_variables_locales
Et l'on écrit à la fin de la procédure :
MOV ESP,EBP //on remet ESP à sa place de départ
POP EBP
RET
Il s'en suit la disposition de pile suivante (sans variables locales) :
ESP + 8* n + 4 : Paramètre-n
...
ESP +12 : Paramètre 2
ESP +8 : Paramètre 1
ESP +4 : Adresse de retour
ESP +0 : valeur ancienne de EBP
Et relativement à EBP (avec ou sans variables locales):
EBP + 8* n + 4 : Paramètre-n
...
EBP +12 : Paramètre 2
EBP +8 : Paramètre 1
EBP +4 : Adresse de retour
EBP +0 : valeur ancienne de EBP
EBP -4 : variable locale 1
EBP -8 : variable locale 2
...
EBP -4* m : variable locale m
Un appel de fonction se déroule comme suit, indépendamment de la convention d'appel :
Je ne sais pas si vous avez remarqué le nombre de AddressOf que l'on peut faire pour les objets légers, mais cela veut dire que l'on a des pointeurs de fonctions dans la vtable. Nous pouvons donc penser à utiliser une vtable pour appeler des pointeurs de fonctions. Le seul problème restant est que l'état de la pile et des registres change suivant la convention d'appel...
La méthode sera donc la suivante : on fait créer un objet léger avec une interface possédant une seule fonction qui a la signature (ou a peu près) de la fonction à appeler. On aura donc une vtable à 4 entrées (les méthodes de IUnknown et notre fonction). La quatrième entrée de la vtable sera donc un pointeur vers un morceau de code asm (compilé bien sur) pour transformer un appel méthode COM en un appel de pointeur de fonction avec la bonne convention d'appel...
Mais vous avez dit "convention d'appel"... Sans être indiscret, qu'est-ce que cela peut être ?
Je pars du principe que la valeur de retour tient dans les registres et qu'un pointeur vers la mémoire n'est pas nécessaire. Sinon, ça se complique encore un peu plus. On verra ça en fin de tutorial...
Une convention d'appel est la façon dont les paramètres sont passés à la fonction appelée. Il en existe un certain nombre.
Nom de la convention | Passage des paramètres | Nettoyage de la pile | Décoration des noms | Notes |
Stdcall | Sur la pile de droite à gauche | Appel... | Un _ devant le nom
Un @ et la taille des paramètres après le nom |
Utilisé par VB et par les APIs Windows |
Cdecl | Sur la pile de droite à gauche | Appelant | Un _ devant sauf si extern "C" | Utilisé par défaut par les compilo C |
Fastcall | Dans les registres ECX et EDX et sur la pile de droite à gauche | Appel... | Un @ devant
Un @ et la taille des paramètres après le nom |
Utilisé par les compilo Borland |
Pascal | Sur la pile de gauche à droite | Appel... | ???? | Obsolète |
Thiscall | Sur la pile de droite à gauche et un pointeur This dans ECX | Appel... | Un ? avant
Un @ suivi d'un bazar indiquant la signature de la fonction |
Utilisé pour les objet C++ (class) |
Register | Dans trois registres et sur la pile | Appel... | ???? | Utilisé par Delphi |
C'est la seule convention d'appel que VB supporte de façon native. Tous les paramètres sont passés sur la pile de droite à gauche (par rapport à l'ordre de déclaration). Le premier argument sera donc à ESP+4. Le deuxième argument sera à esp+8... et ainsi de suite...
C'est la fonction qui retire les paramètres de la pile avant le RET.
La pile ressemble à ceci :
Paramètre-n
...
Paramètre-2
Paramètre-1
ReturnAddress
C'est comme la convention stdcall. La seule différence est qu'il y a en plus, le paramètre this de type pointeur (32 bits) qui est toujours en premier .
La pile ressemble à ceci :
Paramètre-n
...
Paramètre-2
Paramètre-1
Thispointer
ReturnAddress
La convention cdecl est identique à stdcall à l'exception que ce n'est pas la fonction qui retire les paramètres de la pile mais la fonction qui appelle la fonction .
La pile ressemble à ceci :
Paramètre-n
...
Paramètre-2
Paramètre-1
ReturnAddress
La seule différence avec la convention stdcall, c'est que les deux premiers paramètres entiers ou pointeurs (<= 4 octets) sont passés dans les registres ECX et EDX. Les types réels et supérieurs à 4 octets sont passés sur la pile. Cela signifie que l'ordre des paramètres ne sera pas forcément le même dans : ECX, EDX, la pile et dans la déclaration.
La pile ressemble à ceci :
Paramètre-n
...
Paramètre-4
Paramètre-3
ReturnAddress
A noter que le paramètre 3 n'est pas forcément celui qui est le troisième dans la déclaration de la fonction. Il faut tenir compte des types qui peuvent entrer dans un registre de CPU.
C'est comme la convention d'appel stdcall et un pointeur This (vers une structure qui n'est pas celle d'un objet COM mais d'une classe C++) se trouve dans ECX. C'est la convention en C++ pour les objets.
C'est comme la convention stdcall avec les paramètres en ordre inverse, passés de gauche à droite.
Je n'expose ici que le principe et pas le code sinon le tutorial ferait 30 pages. Pour retrouver le code complet allez voir les différents modules des versions pile (Stack) et tas (Heap).
L'adresse de la fonction à appeler sera stockée dans la structure de l'objet léger dans le troisième DWORD, c'est à dire à l'offset 8 par rapport au début de la structure.
La pile ressemble à ceci pour un appel d'une méthode d'objet:
Paramètre-n
...
Paramètre-2
Paramètre-1
Thispointer
ReturnAddress
Et nous voulons ceci :
Paramètre-n
...
Paramètre-2
Paramètre-1
ReturnAddress
Il faut donc supprimer le pointeur This de la pile et faire un JUMP à l'adresse de la fonction qui est stockée dans la structure de l'objet (pointée par This) à l'offset 8.
Il faudra donc le code suivant en ASM :
pop ecx //conserve lareturn address
pop eax //conserve le pointeur this
push ecx //remetd'adresse de retour sur la pile
jmp DWORD ptr [eax + 8] //on appellela fonction
Voici donc un moyen d'appeler des fonctions stdcall par un objet.
Le code suivant n'est pas spécifique à la fonction appelée.On pourra donc le mettre dans une variable globale au module. Il faudra déclarer un type contenant un tableau fixe de Long (pour être aligné) afin d'être sur que la variable ne soit pas détruite avant l'objet qui pourrait encore en avoir besoin. Et pourquoi pas une constante ? Parce que le code exécutable doit être dans une zone mémoire en lecture écriture...
Nous aurons donc pour tous les code ASM, le type suivant :
Type asmCode Code(0 To Taille ) As Long End Type Private m_asmCode as asmCode
On ajustera Taille au nombre de DWORDs nécessaires au code compilé. Si le dernier morceau du code fait moins qu'un DWORD, on ajoutera des NOPs (&H90) ou des int 3 (&HCC) après le code ASM
Comme pour stdcall, il faut aussi retirer le pointeur This de la pile avant d'appeler la fonction. Le problème restant est q'au retour de la fonction appelée, les paramètres ne seront pas retirés de la pile. Il faut donc pouvoir exécuter du code juste après léappel à la fonction avant de rendre la main à VB.
Ajoutons à tout cela, que dans un soucis d'encapsulation, la taille des paramètres à retirer de la pile est propre à une fonction, donc à un objet. Le code de suppression des paramètres de la pile devra se trouver dans la structure de l'objet.
Il faudra donc :
On aura une structure d'objet comme suit :
Private Type typFunctionCallerHeap 'le pointeur vers la vtable pVTable As Long 'le compteur de référence pour savoir quand on doit libérer la mémoire de l'objet cCount As Long 'données attachées '-------------------- 'un pointeur vers la fonction lpfn As Long 'la taille des arguments de la fonction cbArgSize As Long 'le code ASM supplémentaire lpPushReturnAddressAs Long lpRetAddress As Long lpRetAs Long EndType
Cela donne le code ASM suivant :
pop ecx //l'adresse de retour finale
pop eax //pointeur this
mov [eax + 20 * ,ecx //stocke l'adresse de retour finale
lea ecx,[eax + 16 * //charge l'adresse du code de retour pour libérer la pile
push ecx //on met cette adresse de retour sur la pile
jmp dword ptr [eax + 8 * //on va dansla fonction à appeler
//code à mettre dans l'objet
push 0x12345678 //on remet l'adresse de retour finale sur la pile :
//l'objet est construit de façon à ce que l'adresse
//de retour finale stockée
//vienne remplacer 0x12345678
ret0x1234 //on retourne à l'adresse finale en supprimant
//les paramètres empilés
//l'objet est construit pour que 0x1234 soit
//remplacé par lataille des paramètres
Alors là, ça se complique au niveau prototype de fonction, car il faut mettre les paramètres, non pas dans leur ordre de définition mais dans l'ordre de la convention fastcall :
Par exemple, si l'on a la déclaration suivante :
IntFct(double d, int a, float f, int* b, int c);
On mettra la déclaration suivant dans le fichier ODL :
Int Fct([in]int a,[in,out]int*b,[in]double d,[in]float f,[in]int c);
Une fois les paramètres dans le bon ordre, il ne reste plus qu'à :
La structure de l'objet sera la suivante :
Private Type typFunctionCallerHeap 'le pointeur vers la vtable pVTable As Long 'le compteur de référence pour savoir quand on doit libérer la mémoire de l'objet cCount As Long 'données attachées '-------------------- 'un pointeur vers la fonction lpfn As Long 'stockage de l'adresse de retour lors de l'appel lpRet As Long 'nombre d'arguments entiers et pointeurs cArgCount As Long 'code ASM pour gérer l'appel asmCode(0 To5) As Long 'puisque le code ASM est différent pour chaque objet, la vtable aussi VTable As VTable End Type
Cela nous donne le code suivant :
pop ecx //conserve la return address
pop eax //conserve le pointeur this
mov [eax + 12], ecx //on conserve temporairement l'adresse de
//retour dans l'objet
//premier paramètre dans ECX : si pas présent NOP
pop ecx
//deuxième paramètre dans EDX : si pas présent NOP
pop edx
push [eax + 12] //on remet l'adresse de retour sur la pile
jmp dword ptr [eax + 8] //on va dans la procédure
Comme le code ASM dépend de la fonction à appeler, on doit mettre le code (et donc la vtable qui pointe vers) dans l'objet.
Le plus dur est de connaître la structure de l'objet C++, à savoir les variables privées. Il faut pour cela, disposer du fichier .h et traduire les membres de type variable de l'objet en structure VB. Cette variable est à passer au constructeur de l'objet COM d'appel de la méthode de la classe C++. Il ne faut pas confondre le pointeur This des objets COM (et VB) avec le pointeur This des objets C++. Ce dernier a un pointeur vers une vtable, uniquement s'il contient des fonctions virtuelles. Un objet C++ n'est pas une entité autonome au sens de COM. La seul différence entre une fonction stdcall et une méthode thiscall est la décoration de son nom par le compilateur et le pointeur this dans ECX.
Le code est pratiquement celui du code pour stdcall puisqu'il faut seulement mettre un pointeur vers une structure de l'objet C++ dans ECX avant l'appel (ce pointeur This est stocké dans la structure de l'objet COM après l'adresse de la fonction):
pop ecx //conserve lareturn address
pop eax //conserve le pointeur this
push ecx //remet l'adresse de retour sur la pile
mov ecx,[eax + 12] //on copie le pointeur This de l'objet C++
jmp DWORD ptr [eax + 8] //on appelle la méthode de l'objet C++
C'est exactement le même code que pour stdcall à l'exception que les paramètres de la fonctions doivent être écrits (dans le fichier ODL) à l'envers de leur déclaration en pascal. SI les paramètres sont a, b, c en pascal, il seront c, b, a dans le fichier ODL.
Dans ce cas, un pointeur vers la zone mémoire prévue pourla valeur de retour est passé :
Il suffit donc de déclarer ce paramètre dans la liste des paramètres comme un long.
Pour stdcall, cdecl, thiscall et pascal, on ajoute un paramètre Long en premier paramètre.
Pour fastcall, on met le paramètre Long après les deux premiers paramètres pouvant être passés dans les registres (s'il y en a). Ce paramètre de valeur de retour n'est jamais passé par registres.