Appeler des pointeurs de fonctions depuis vb

Appeler des pointeurs de fonctions depuis vb

Rappels : les registres, le mode protégé; et lapile d'exécution

Un CPU x86, c'est à dire la pluspart des processeurs (Intelet AMD) des PCs, comporte des registres qui sont les suivants :

  • Registres généraux
    • EAX, le registre à tout faire et utilisé pour le retour de la valeur lors d'appel de fonction (instruction CALL).Il n'est donc pas sauvegardé lors d'appels de fonctions
    • EBX, registre qui doit être sauvegardé avant utilisation dans une fonction
    • ECX, registre compteur, utilisé pour les boucles LOOP et REP. Il n'est pas sauvegardé
    • EDX, registre utilisé pour le retour de valeur de 64 bits (avec EAX). Il n'est pas sauvegardé.
  • Registres d'index : ils sont sauvegardés pendant les appels de fonctions
    • ESI, est le registre qui sert à contenir l'adresse 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 l'adresse 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, c'est à dire la dernière valeur empilée
    • EBP, est le registre qui sert dans les fonctions, à conserver l'adresse de pile du début de léappel de façon à ce que les paramètres gardent le même offset (relativement à EBP) si l'on doit faire des PUSH et des POP (qui modifient ESP). Si l'on utilise ESP, on doit tenir compte de ses variations. Ce registre est conservé lors des appels.
  • Registre pointeur d'instruction
    • EIP,est le registre qui pointe la prochaine instruction à exécuter. Il n'est pas accessible ni modifiable directement.

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 :

  • Tous les arguments sont ajustés à la taille du bus de donnée soit 32 bits pour les PC
  • Tous les arguments sont placés soit sur la pile, soit dans des registres
  • L'adresse de retour est empilée sur la pile
  • Un saut est fait vers la fonction
  • La fonction crée son 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 s'exé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 l'adresse 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 d'appel, la fonction retire les paramètres de la pile et rend la main à l'appelant, 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 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...

Les conventions d'appels

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

La convention stdcall

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

La convention d'appel des méthodes COM

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 d'appel cdecl

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 convention d'appel fastcall

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.

La convention d’appel thiscall

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.

La convention d'appel pascal

C'est comme la convention stdcall avec les paramètres en ordre inverse, passés de gauche à droite.

Transformer un stdcall méthode en ...

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.

...stdcall

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

...Cdecl

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 :

  • Retirer le pointeur This de la pile
  • Conserver l'adresse de retour finale dans l'objet pour compléter le code ASM inclu
  • Appeler la fonction
  • Au retour, on se retrouve dans l'objet :
    • On remet sur la pile, l'adresse de retour définitif
    • On fait un RET avec une taille de paramètre contenue dans l'objet

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

...Fastcall

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 :

  • S'il 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 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'à :

  • Retirer le pointeur This (comme toujours)
  • On met les deux premiers paramètres dans les registres ECX et EDX si besoin
  • On remet l'adresse de retour
  • On fait un saut dans le fonction

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.

...Thiscall

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++

...Pascal

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.

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 n'importe quel convention d'appel
  • TOUJOURS avant tout autre paramètre pour n'importe quel convention d'appel

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.

Ce document intitulé « Appeler des pointeurs de fonctions depuis vb » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous