Un petit rappel sur les registres, le mode protégé et lapile dexécution
Un CPU x86, cest à dire la plus part des processeurs (Intelet AMD) des PCs, comporte des registres qui sont les suivants :
o EAX, le registre à tout faire et utilisépour le retour de la valeur lors dappel de fonction (instruction CALL).Il nest donc pas sauvegardé lors dappels de fonctions
o EBX, registre qui doit être sauvegardé avantutilisation dans une fonction
o ECX, registre compteur, utilisé pour les bouclesLOOP et REP. Il nest pas sauvegardé
o EDX, registre utilisé pour le retour devaleur de 64 bits (avec EAX). Il nest pas sauvegarder
- Registres dindex : ils sont sauvegardés pendant les appels de fonctions
- ESI est le registre qui sert à contenir ladresse source pour la copie de mémoire avec les instructions MOVS (dites de chaines) (et aussi, STOS, LODS, SCAS et CMPS)
- EDI est le registre qui sert à contenir ladresse destination pour la copie de mémoire avec les instructions MOVS (et aussi, STOS, LODS, SCAS et CMPS)
- Registres de piles
- ESP est le registre qui sert à pointer le sommet de la pile, cest à dire la dernière valeur empilée
- EBP est le registre qui sert dans les fonctions, à conserver ladresse de pile du début de lappel de façon à ce que les paramètres gardent le même offset (relativement à EBP) si lon doit faire des PUSH et des POP (qui modifient ESP). Si lon utilise ESP, on doit tenir compte de ses variations. Ce registre est conservés lors des appels.
- Registre pointeur dinstruction
- EIP est registre qui pointe la prochaine instruction à exécuter. Il nest pas accessible ni modifiable directement.
Si vous avez déjà entendu parlé de registres de segmentsen mode réel, tels que CS, DS, ES, FS, GS, SS
et bien oubliezles, car en mode 32bits protégé, il ny a pas de segments
ou plutôt, unseul de 4Go (en théorie)
La pile sert à stocker temporairement les informationsnécessaires au fonctionnement du programme : adresse de retour defonction, variables locales
La pile est une zone mémoire qui croit vers le bas.Au départ, la pile est au plafond et à chaque fois que lon empile, le sommetdescend de 4 octets du plafond. Lorsque lon dépile, le sommet remonte de 4octets. Il sen 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 lon fait un appel de fonction, ladresse delinstruction suivant le CALL (adresse de retour) est poussée sur la pile, puissuivent les paramètres suivant la convention dappel. La pile ressembledonc à 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 lon utilise EBP, on écrira le code suivant au début dela procédure :
PUSHEBP //on doit sauvegarder EBP avantchangement
MOV EBP,ESP//on garde lemplacement de pile actuel
SUB ESP,taille_des_variables_locales
Et lon écrit à la fin de la procédure :
MOV ESP,EBP//on remet ESP à sa place de départ
POP EBP
RET
Il sen suit la disposition de pile suivante (sansvariables 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épendammentde la convention dappel :
- Tous les arguments sont ajustés à la taille du bus de donnée soit 32bits pour les PC
- Tous les arguments sont places soit sur la pile, soit dans des registres
- Ladresse de retour est empilée sur la pile
- Un saut est fait vers la fonction
- La fonction crée sont cadre de pile avec EBP si nécessaire (si oui, elle sauvegarde EBP)
- Si la fonction utilise ESI, EDI et EBX, elle les sauvegarde sur la pile
- La fonction sexécute
- La valeur de retour est mise :
- Dans le registre EAX pour des entiers ou pointeurs <= 4octets
- Dans les registres EDX :EAX pour des entiers ou pointeurs de 8 octets
- Dans les registres du coprocesseur arithmétique pour les réels (ST(0))
- Dans une zone mémoire dont ladresse est passée en tout premier paramètre (toujours sur la pile) à la fonction pour des valeur de retour de taille > 8 octets. Cette adresse est renvoyée dans EAX.
- Les registres sauvegardés sont restaurés
- Le cadre de pile est restauré
- Suivant la convention dappel, la fonction retire les paramètres de la pile et rend la main à lappelant, OU rend simplement la main sans retirer les paramètres.
Les objets légers et les pointeurs de fonctions
Je ne sais pas si vous avez remarquer le nombre de AddressOfque lon peut faire pour les objets légers
mais cela veut dire que lon a despointeurs de fonctions dans la vtable
Nous pouvons donc penser à utiliser unevtable pour appeler des pointeurs de fonctions
le seul problème restant est quelétat de la pile et des registres change suivant la convention dappel
La méthode sera donc la suivante : on fait crée unobjet léger avec une interface possédant une seule fonction qui à la signature(ou à peu près) de la fonction à appeler. On aura donc une vtable à 4 entrées (lesméthodes de IUnknown et notre fonction). La quatrième entrée de lavtable sera donc un pointeur vers un morceau de code asm (compilé biensûr) pourtransformer un appel méthode COM en un appel de pointeur de fonction avec labonne convention dappel
Mais vous avez dit « convention dappel »
sansêtre indiscret, quest-ce que cela peut être ?
Je part du principe que la valeur de retour tient dans lesregistres et quun pointeur vers le la mémoire nest pas nécessaire
Sinon, çase complique encore un peu plus
on verra ça en fin de tutorial
Les conventions dappels
Une convention dappel est la façon dont les paramètres sontpassé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 dun 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 |
La convention stdcall
Cest la seule convention dappel que VB supporte defaçon native. Tous les paramètres sont passés sur la pile de droite àgauche (par rapport à lordre de déclaration). Le premier argument seradonc à ESP+4. Le deuxième argument sera à esp+8
et ainsi desuite
Cest la fonction qui retire les paramètres de la pile avantle RET.
La pile ressemble à ceci :
Paramètre-n
Paramètre-2
Paramètre-1
ReturnAddress
La convention dappel des méthodes COM
Cest comme la convention stdcall. La seule différence estquil y a en plus, le paramètre this de type pointeur (32 bits) qui esttoujours en premier.
La pile ressemble à ceci :
Paramètre-n
Paramètre-2
Paramètre-1
Thispointer
ReturnAddress
La convention dappel cdecl
La convention cdecl est identique à stdcall à lexceptionque ce nest pas la fonction qui retire les paramètres de la pile mais lafonction qui appelle la fonction.
La pile ressemble à ceci :
Paramètre-n
Paramètre-2
Paramètre-1
ReturnAddress
La convention dappel fastcall
La seule différence avec la convention stdcall, cest que lesdeux premiers paramètres entiers ou pointeurs (<= 4 octets) sont passés dansles registres ECX et EDX. Les types réels et supérieurs à4 octets sont passés sur la pile. Cela signifie que lordre des paramètresne sera pas forcement le même dans : ECX, EDX, la pile
etdans la déclaration.
La pile ressemble à ceci :
Paramètre-n
Paramètre-4
Paramètre-3
ReturnAddress
À noter que le parameter 3 nest pas forcement celui quiest le troisième dans la déclaration de la fonction. Il faut tenir compte destypes qui peuvent entrer dans un registre de CPU.
La convention dappel thiscall
Cest comma la convention dappel stdcall et un pointeurThis (vers une structure qui nest pas celle dun objet COM mais duneclasse C++) se trouve dans ECX. Cest la convention en C++ pour lesobjets.
La convention dappel pascal
Cest comme la convention stdcall avec les paramètres enordre inverse, passés de gauche à droite.
Transformer un stdcall méthode en
Je nexpose ici que le principe et pas le code sinon letutorial ferait 30 pages. Pour retrouver le code complet allez voir lesdifférents modules des versions pile (Stack) et tas (Heap).
Ladresse de la fonction à appeler sera stocké dans lastructure de lobjet léger dans le troisième DWORD, cest à dire à loffset 8par rapport au début de la structure.
Stdcall
La pile ressemble à ceci pour un appel dune méthodedobjet:
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 etfaire un JUMP à ladresse de la fonction qui est stockée dans la structurede lobjet (pointée par This) à loffset 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 dappeler des fonctions stdcall par unobjet.
Le code suivant nest pas spécifique à la fonction appelée.On pourra donc le mettre dans une variable globale au module. Il faudradéclarer un type contenant un tableau fixe de Long (pour être aligné) afindêtre sûr que la variable ne soit pas détruite avant lobjet qui pourraitencore en avoir besoin. Et pourquoi pas une constante ? Parce que le codeexécutable doit être dans une zone mémoire en lecture écriture
Nous aurons donc pour tous les code ASM, le typesuivant :
Type asmCode
Code(0 To Taille) As Long
End Type
Private m_asmCode as asmCode
On ajustera Taille au nombre de DWORDs nécessaires aucode compilé. Si le dernier morceau du code fait moins quun DWORD, on ajouterades NOPs (&H90) ou des int 3 (&HCC) après le code ASM
Cdecl
Comme pour stdcall, il faut aussi retirer le pointeurThis de la pile avant dappeler la fonction. Le problème restant estqau retour de la fonction appelée, les paramètres ne seront pas retirés dela pile. Il faut donc pouvoir exécuter du code juste après lappel àla fonction avant de rendre la main à VB.
Ajoutons à tout cela, que dans un soucis dencapsulation, lataille des paramètres à retirer de la pile est propre à une fonction, donc à unobjet. Le code de suppression des paramètres de la pile devra se trouver dansla structure de lobjet.
Il faudra donc :
- Retirer le pointeur This de la pile
- Conserver ladresse de retour finale dans lobjet pour compléter le code ASM inclu
- Appeler la fonction
- Au retour, on se retrouve dans lobjet :
- On remet sur la pile, ladresse de retour définitif
- On fait un RET avec une taille de paramètre contenue dans lobjet
On aura une structure dobjet comme suit :
PrivateType typFunctionCallerHeap
'le pointeur vers la vtable
pVTable As Long
'le compteur de référence pour savoirquand 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 deretour 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 retourpour
//libérer la pile
push ecx //on met cetteadresse 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 retourfinale stockée
//vienneremplacer 0x12345678
ret0x1234 //onretourne à l'adresse finale en supprimant //les paramètres empilés
//l'objet est construit pour que 0x1234 soit //remplacé par lataille des paramètres
Fastcall
Alors là, ça se complique au niveau prototype defonction
car il faut mettre les paramètre, non pas dans leur ordre dedéfinition mais dans lordre de la convention fastcall :
- Sil y a un ou plusieurs paramètres entiers ou pointeurs (<= 4 octets), on les met en premier
- On met donc tout autres paramètres en suivant
Par exemple, si lon 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 plusquà :
- Retirer le pointeur This (comme toujours)
- On met les deux premiers paramètres dans les registres ECX et EDX si besoin
- On remet ladresse de retour
- On fait un saut dans le fonction
La structure de lobjet sera la suivante :
PrivateType typFunctionCallerHeap
'le pointeur vers la vtable
pVTable As Long
'le compteur de référence pour savoirquand 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 del'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 pourchaque objet, la vtable aussi
VTable As VTable
End Type
Cela nous donne le code suivant :
pop ecx //conserve lareturn 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 pasprésent NOP
pop ecx
//deuxième paramètre dans EDX : sipas présent NOP
pop edx
push [eax + 12] //on remetl'adresse de retour sur la pile
jmp dword ptr [eax + 8] //on va dansla procédure
Comme le code ASM dépend de la fonction à appeler, on doitmettre le code (et donc la vtable qui pointe vers) dans lobjet.
Thiscall
Le plus dure est de connaître la structure de lobjet C++, àsavoir les variables privées. Il faut pour cela, disposer du fichier .h ettraduire les membres de type variable de lobjet en structure VB. Cettevariable est à passer au constructeur de lobjet COM dappel de la méthode dela classe C++. Il ne faut pas confondre le pointeur This des objets COM (etVB) avec le pointeur This des objets C++. Ce dernier a un pointeur versune vtable, uniquement sil contient des fonctions virtuelles. Un objet C++nest pas une entité autonome au sens de COM. La seul différence entre unefonction stdcall et une méthode thiscall est la décoration de son nom par lecompilateur et le pointeur this dans ECX.
Le code est pratiquement celui du code pour stdcallpuisquil faut seulement mettre un pointeur vers une structure de lobjet C++dans ECX avant lappel (ce pointeur This est stocké dans la structure delobjet COM après ladresse de la fonction):
pop ecx //conserve lareturn address
pop eax //conserve le pointeur this
push ecx //remetd'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 appellela méthode de l'objet C++
Pascal
Cest exactement le même code que pour stdcall à lexceptionque les paramètres de la fonctions doivent être écrits (dans le fichier ODL) àlenvers de leur déclaration en pascal. SI les paramètres sont a, b, c enpascal, il seront c, b, a dans le fichier ODL.
Si la fonction renvoie un type de taille supérieure à 8 octets
Dans ce cas, un pointeur vers la zone mémoire prévue pourla valeur de retour est passé :
- TOUJOURS sur la pile pour nimporte quel convention dappel
- TOUJOURS avant tout autre paramètre pour nimporte quel convention dappel
Il suffit donc de déclarer ce paramètre dans la liste desparamètres comme un long.
Pour stdcall, cdecl, thiscall et pascal, on ajoute unparamètre Long en premier paramètre.
Pour fastcall, on met le paramètre Long après lesdeux premiers paramètres pouvant être passés dans les registre (sil y en a).Ce paramètre de valeur de retour nest jamais passé par registres.