Nono40.developpez.com
Le petit coin du web de Nono40
SOURCES TESTS DELPHI WIN32 AUTOMATISMES DELPHI .NET QUICK-REPORT

Utilisation de l'assembleur en ligne avec Delphi

Ce document présente l'utilisation de l'assembleur en ligne intégré dans Delphi. Il décrit en détail l'utilisation en assembleur des principaux type de données utilisés en pascal.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Introduction

Ce document est destiné à tous ceux qui souhaitent utiliser de l'assembleur dans Delphi. Ceci peut dans certains cas être plus rapide que l'écriture en langage évolué. Ce document n'est pas un cours d'initiation à l'assembleur, il suppose que vous connaissiez la structure des registres et le langage des processeurs x86 et compatibles. De même il est supposé que vous avez une bonne connaissance du pascal. Il n'est pas nécessaire d'avoir des notions précises des modes Réel, Protégé et Virtuel du 386, ni de la structure segmentée de la mémoire propre aux processeurs x86.

Ce document ne présente pas non plus de méthode d'optimisation grâce à l'assembleur. Mais plutôt une description détaillée de l'interfaçage de l'assembleur avec le pascal.

Vu l'époque actuelle, et le peu de systèmes 16 bits encore en service, tout ce document ne porte que sur Delphi 2 et suivant. C'est-à-dire sur l'adressage 32 bits des processeurs. L'utilisation de fonctions récentes (SSE2) requiert Delphi 6.

I. Programmer en assembleur sous Delphi

I-A. Inclure du code assembleur

Pour intégrer de l'assembleur dans le code Pascal de Delphi, il suffit de l'entourer des mots clefs Asm et End. Toutes les instructions situées entre ces mots doivent être des instructions assembleur. Elles sont insérées tel que dans le code exécutable.

Exemple simple :

 
Sélectionnez

procedure TForm1.Button2Click(Sender: TObject);
Var a,b:Integer;
begin
  A:=1;
  B:=2;
  Asm
    Mov EAX,A
    Add B,EAX
  End;
  Label1.Caption:=IntToStr(b);
end;

I-B. Quelles sont les instructions utilisables ?

Delphi7 intègre dans l'assembleur en ligne tout le jeu d'instructions défini par Intel. Que ce soient les instructions de base, FPU, MMX, SSE et SSE2. (Bien sûr toutes ne sont pas utilisables suivant les processeurs !) Delphi est en mode d'adressage 32bits, c'est-à-dire qu'il vaut mieux utiliser les registres 32 bits par défaut. Il est toutefois possible de travailler avec les registres 8 et 16 bits, Delphi ajoutant les préfixes nécessaires pour les opérations. De même l'adressage étendu est utilisé par défaut, c'est-à-dire que les adressages suivants sont autorisés :

 
Sélectionnez

Mov EAX,BaseTableau[ECX+EDX*4]
Mov EAX,Dword ptr [ESI+EDI+20]

I-C. Quels sont les registres utilisables ?

Tous les registres définis dans la structure IA32 des processeurs Intel sont utilisables :

  • Registres communs : EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP ainsi que les registres AX,BX,CX,DX,SI,DI,BP,SP,AL,AH,BL,BH,CL,CH,DL,DH
  • Registre d'état EFLAGS
  • Registres de segment : DS ES FS GS CS SS, bien sur il est rare d'avoir à utiliser ses registres dans Delphi. Ce n'est recommandé qu'aux utilisateurs avancés, et inutile dans la plupart des cas. ( voir plus bas )
  • Registres FPU : ST0 à ST7
  • Registres MMX : MM0 à MM7
  • Registres SSE/SSE2 : XMM0 à XMM7 et MXCSR

Tous les registres sont utilisables dans le code assembleur, par contre vous ne devez pas modifier le contenu des registres suivants : EBX, ESI, EDI, EBP, ESP, DS, ES, CS et SS. En cas de besoin il faut en sauvegarder leur valeur en début de code et la restituer en fin :

 
Sélectionnez

PUSH EBX
PUSH EDI
// Code asm utilisant EBX et EDI
POP EDI
POP EBX

I-D. Sur quoi pointent les registres segments ?

Les registres de segments sont chargés par Delphi. Il n'est pas utile de modifier leur valeur. Delphi fonctionne en mode protégé en adressage 32 bits. Toute la mémoire de l'application est donc visible sans avoir à changer la valeur des segments.

DS et ES pointent sur la zone données de l'application, que ce soient des variables statiques ou des objets. Les instructions de chaînes utilisant ces segments par défaut, il n'est pas utile de surcharger avec les préfixes ES : ou DS : vu que les deux pointent sur la même plage d'adresses

SS pointe sur la mémoire de la pile. Dans une application Delphi SS contient le même descripteur que ES/DS.

CS pointe sur le code, sa valeur doit être utilisée pour l'accès au code.

Notez que dans 99% des cas, la gestion des segments effectuée par Delphi est suffisante et il n'est pas utile de se poser la question du contenu de ces registres.

I-E. Ecrire le code

La syntaxe d'une instruction assembleur est la suivante :

 
Sélectionnez

[Label :] Instruction Opérande1[,Opérande2[,Opérande3]

Le nombre d'opérande doit correspondre avec l'instruction utilisée. Quand différents nombres d'opérandes sont utilisables pour une même instruction, Delphi encode l'instruction correspondant au nombre d'opérandes utilisées. C'est le cas notamment pour l'instruction IMUL.

I-E-1. Opérandes

Les opérandes possibles sont les suivantes. Les combinaisons autorisées dépendent de l'instruction utilisée.

  • Valeur immédiate
  • Registre commun ou spécifique
  • Variables Delphi
  • Adresse mémoire directe ou indexée

Pour les valeurs immédiates, les règles d'évaluation à la compilation sont les mêmes que pour le pascal. Les valeurs suivantes sont autorisées :

 
Sélectionnez

Const Valeur=123 ;
...
MOV EAX,4
MOV ECX,Valeur
MOV ECX,(Valeur+2)*3

Une valeur immédiate sera toujours de la même taille que le registre/mémoire de destination dans la mesure où Delphi est capable d'en évaluer la taille. Dans le cas contraire il faut forcer la taille de l'opération en utilisant l'une des directives BYTE PTR, WORD PTR etc.

 
Sélectionnez

MOV BYTE PTR [EDI],3

La taille des opérandes doit correspondre à l'instruction utilisée et les tailles des opérandes doivent correspondre entre elles. Delphi ne vérifie que la taille en octets des opérandes et non pas son type exact. Pas exemple les types Integer, Dword et Array[0..3] of byte sont identiques au niveau de l'assembleur. Exemples de concordance de taille d'opérande :

 
Sélectionnez

Var a:Integer;
    b:Word;
    C:Array[0..3]Of Byte;
    R8:Array[0..7]Of Byte;
    R16:Array[0..15]Of Byte;
    R:Record
        rI:Integer;
        rD:Integer;
      End;
begin
  Asm
    MOV    EAX,EBX
    MOV    EAX,A
    MOV    CX,B
    MOV    EAX,C
    MOV    EAX,R.rD
    MOVQ   MM1,R8
    MOVDQU XMM1,R16
  end;
end;

Attention c'est le type de variable directement qui est utilisé pour la concordance. Dans le cas d'un tableau, c'est la taille du type complet même si on accède à un indice, car l'indice n'est pas interprété comme indice du tableau, mais comme un Offset supplémentaire ajouté à l'adresse de base du tableau.

 
Sélectionnez

Var
    T4:Array[0..10]Of Integer;
Begin
  Asm
    MOV EAX,T4[2] // Incorrect
  End;
End;

Dans le cas d'un adressage indirect, ou dans le cas d'un transtypage il est possible de forcer la taille de l'opérande à l'aide d'une des directives suivantes :

 
Sélectionnez

Var Buff:Array[0..511]Of Byte;
begin
  Asm
    MOV    AL,Byte ptr Buff      // 1  octet
    MOV    AX,Word ptr Buff      // 2  octets
    MOV    EAX,DWord ptr Buff    // 4  octets
    MOVQ   MM0,QWord ptr Buff    // 8  octets
    MOVDQU XMM1,DQWord ptr Buff  // 16 octets

    FLD    QWord ptr Buff        // 8  octets
    FLD    TByte ptr Buff        // 10 octets
  end;
end;

I-E-2. Labels

Les labels sont définis comme en pascal à l'aide du mot clef label :

 
Sélectionnez

Label Retour ;
…
Asm
  Retour: ADD EAX,Tableau[ECX*4]
          LOOP Retour
End;

Il est possible d'utiliser un label commençant par @, dans ce cas il n'est pas nécessaire de le définir dans une clause label. Mais il n'est alors utilisable que dans le même bloc ASM.

 
Sélectionnez

Asm
  JMP @LAB1 // Correct
  JMP @LAB2 // Interdit
  …
@LAB1: ADD EAX,EDX
End
Inc(UneVariable) ;
Asm
@LAB2: ADD EAX,ECX
End;

I-E-3. Directives

Les directives suivantes sont utilisables dans le code assembleur : DB,DW,DD,DQ : Insertion en ligne de données de type byte, word, dword ou quadword
Exemples :

 
Sélectionnez

DB 1
DB 'A'
DD 12345678
DQ $ABCDEFGH12345678

Ces directives intègrent directement les valeurs dans le code à l'endroit où elles sont données. Il est préférable d'utiliser des constantes dans une zone const pour définir ce genre de valeur. La directive DB permet aussi d'inclure du code machine " inline ", cette méthode est déconseillée mais peut servir dans certains cas particuliers.

Les directives VMTOFFSET et DMTINDEX permettent d'accéder aux méthodes virtuelles et dynamiques des objets. ( voir le chapitre sur les objets )

I-E-4. Opérateurs spéciaux

Les opérateurs suivants peuvent être utilisés sur les opérandes :

  • Low : retourne l'octet de poids faible de l'opérande. Ne pas confondre avec la fonction Low() du pascal.
  • High : retourne l'octet de poids fort de l'opérande. Ne pas confondre avec la fonction High() du pascal.
  • Type : Retourne la taille en octet de l'opérande. C'est l'équivalent de la fonction SizeOf() du pascal.
  • : : Opérateur de surcharge du segment par défaut d'une instruction.
  • [ ] : Offset ajouté à l'adressage en cours, il peut contenir des registre, un identificateur ou une valeur immédiate. L'offset ajouté est toujours calculé en octets.

I-E-5. Commentaires

L'ajout de commentaires dans le code assembleur est effectué par les mêmes balises qu'en pascal. De plus ceux-ci ne peuvent être placés en milieu de ligne. Le commentaire assembleur de fin de ligne ';' n'est pas reconnu, il doit être remplacé par //.

Remarque : l'assembleur étant moins lisible que le pascal, il est fortement conseillé de bien commenter le code.

II. Accès aux différents types de données

II-A. Généralités

Tous les types de données peuvent être utilisés en assembleur. Cependant tous les types statiques sont beaucoup plus simples à utiliser. Les sections suivantes donnent en détail l'utilisation de ces principaux types.
Les types dynamiques comme String ou Array Of -type-, sont d'un usage plus compliqué et nécessitent l'usage de fonctions internes à Delphi non documentées. Ils ne seront donc pas décrits ici.
Dans la plupart des cas l'utilisation des types de données peut être fait sans utilisation de la directive xxx PTR. L'erreur de compilation " Non concordance de taille d'opérande " signale le plus souvent une erreur dans le choix d'un registre ou la taille d'une mémoire.

II-B. Types ordinaux

Les ordinaux sont tous les types équivalents à une valeur entière. C'est-à-dire les types entiers, énumérés, booléens, caractères ou pointeurs.

L'utilisation de ces types est simple, l'assembleur travaille toujours sur leur représentation entière.

 
Sélectionnez

Type
  TMonEnum=(Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche);
Var
  A,F:Integer;
  B:Byte;
  C:Char;
  D:Word;
  Enum:TMonEnum;
begin
  Asm
    MOV    EAX,A
    ADD    EAX,F
    MOV    AL,B
    MOV    CL,C
    MOV    AL,Enum;
    CMP    AL,Vendredi
  end;
end;

Attention le type énuméré est équivalent à Byte, Word ou Dword suivant la valeur la plus grande qu'il contient.

 
Sélectionnez

Type
  TEnumByte=(Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche);
  TEnumWord=(Zero,Mille=1000,DeuxMille=2000,TroisMille=3000);
  TEnumDWord=(A,B=100000,C=200000);
Var
  EnumB:TEnumByte;
  EnumW:TEnumWord;
  EnumD:TEnumDWord;
begin
  Asm
    MOV    AL,EnumB; // byte
    CMP    AL,Vendredi
    MOV    AX,EnumW; // word
    CMP    AX,DeuxMille
    MOV    EAX,EnumD; // DWord
    CMP    EAX,C
  end;
end;

II-C. Types ensembles

Les ensembles sont représentés en mémoire par une suite de bits donc chaque bit donne l'appartenance de l'élément à l'ensemble. Le nombre d'octets utilisés dépend du nombre maximum d'éléments de l'ensemble. Ce nombre peut aller de 1 à 32 octets (de 1 à 256 éléments). Les petits ensembles (moins de 32 valeurs ) peuvent être manipulés directement. En exemple un ensemble de 7 valeurs (stocké sur un type byte, car moins de 8 valeurs )

 
Sélectionnez

Type
  TJour    =(Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche);
  TEnsCourt=Set Of TJour;
Var
  LesJours1:TEnsCourt;
  LesJours2:TEnsCourt;
begin
  Asm
    MOV    AL,LesJours1
    OR     AL,LesJours2
  end;
End;

De même les ensembles de 9 à 16 valeurs sont utilisables comme un type Word, les ensembles de 17 à 32 valeurs sont utilisables comme un type DWord.

Pour 33 valeurs et plus, il n'y a plus de correspondance directe. Leur manipulation doit être effectuée en plusieurs étapes avec un transtypage :

 
Sélectionnez

Type
  TListe  = 0..63;
  TEnsLong =Set Of TListe;// Ensemble de 64 valeurs
Var
  EnsLong1:TEnsLong;
  EnsLong2:TEnsLong;
begin
  Asm
    MOV   EAX,DWord ptr EnsLong1
    OR    EAX,DWord ptr EnsLong2
    MOV   EAX,DWord ptr EnsLong1[4]
    OR    EAX,DWord ptr EnsLong2[4]
  end;
end;

II-D. Types réels

Les types réels sont Single, Double, Extended, Comp et Currency. Ces types sont utilisés de façon native par l'unité de calcul flottant intégrée aux processeurs. Il est fortement conseillé de manipuler ces données avec le FPU. L'échange de ces types avec les registres FPU est direct :

 
Sélectionnez

Var
  S1,S2:Single;
  D1,D2:Double;
  E1,E2:Extended;
  C1,C2:Currency;
begin
  Asm
    FLD  S1 // Single
    FADD S2
    WAIT
    FST  S1

    FLD  D1 // Double
    FADD D2
    WAIT
    FST  D1

    FLD  E1 // Extended
    FLD  E2
    FADD
    WAIT
    FSTP E1

    FLD  C1 // Currency
    FLD  C2
    FADD C2
    WAIT
    FST  C1
  end;
end;

Note : le type Real48 n'est pas décrit ici. C'est un type réel non natif présent simplement pour la compatibilité avec les programmes antérieurs à Delphi.

II-E. Type Int64

Le type Int64 ne peut être traité directement par les registres communs ou les opérations mémoires classiques. Dans la plupart des cas le transtypage en deux Dword est utilisé.

 
Sélectionnez

Var
  A:Int64;
  B:Int64;
begin
  Asm // A:=A+B
    MOV EAX,DWord ptr B
    ADD DWord ptr A,EAX
    MOV EAX,DWord ptr B[4]
    ADC DWord ptr A[4],EAX
  end;
end;

Certaines instructions FPU utilisent des valeurs entières sur 64 bits, dans ce cas le type Int64 est utilisé directement :

 
Sélectionnez

Var
  A:Int64;
  D:Double;
begin
  Asm // D:=D+A
    FLD  D // Valeur flottante
    FILD A // Valeur entière
    FADDP
    WAIT
    FST  D
  end;
end;

Le type Int64 est aussi compatible avec les instructions MMX ou SSE portant sur des entiers 64 bits :

 
Sélectionnez

Var
  A:Int64;
  B:Int64;
begin
  Asm // A:=A+B
    MOVQ  MM0,A
    PADDQ MM0,B
    MOVQ  A,MM0
  end;
end;

II-F. Tableaux statiques

Les tableaux sont considérés comme un type dont la longueur est la taille totale du tableau. Pour accéder à un seul élément du tableau, le transtypage est systématique. Le premier élément du tableau est toujours avec un décalage nul par rapport à l'adresse de base du tableau. Par exemple les deux tableaux suivants sont stockés de la même manière en mémoire :

 
Sélectionnez

Var
  Tab1 :Array[0..3]Of Integer ;
  Tab2 :Array[2..5]Of Integer ;

Attention, l'opérateur [ ] ne signifie pas indice du tableau mais Offset supplémentaire. Pour accéder au deuxième élément de Tab1 il faut écrire Tab1[4]. Dans le cas d'un parcourt du tableau à l'aide d'un index, il faut augmenter l'index à chaque boucle de la taille de l'élément de base du tableau. Il est conseillé d'utiliser l'opérateur Type.

 
Sélectionnez

Var
  Tab1 :Array[1..20]Of Integer;
  Somme:Integer;
begin
  Asm
    XOR  ECX,ECX
    XOR  EAX,EAX
@L1:ADD  EAX,DWord Ptr Tab1[ECX]
    ADD  ECX,Type Integer
    CMP  ECX,Type Tab1
    JB   @L1
    MOV  Somme,EAX
  end;
end;

Il faut penser aussi aux multiplicateurs lors de l'adressage. Ceci peut simplifier les opérations sur des tableaux dont le type de base est de taille 2,4 ou 8 octets.
Exemple d'optimisation de la boucle, en utilisant un coefficient multiplicateur d'index :

 
Sélectionnez

Var
  Tab1 :Array[1..20]Of Integer;
  Somme:Integer;
begin
  Asm
    // Nombre d'élément du tableau
    MOV  ECX,(Type Tab1)/(Type Integer)
    XOR  EAX,EAX
    // -4 Car ECX va Balayer de 20 à 1 au lieu de
    // 19 à 0
@L1:ADD  EAX,DWord Ptr Tab1[ECX*4-Type Integer]
    LOOP @L1
    MOV  Somme,EAX
  end;
end;

Les types tableaux peuvent aussi servir d'opérande dans les opérations MMX/SSE dans le cas d'opérations groupées. Par exemple dans le cas d'instructions MMX sur des groupes les types suivants peuvent être utilisés directement :

 
Sélectionnez

Var
  TabB1,TabB2 :Array[0..7]Of Byte;
  TabW1,TabW2 :Array[0..3]Of Word;
  TabD1,TabD2 :Array[0..1]Of Integer;
begin
  Asm
    // addition des éléments de TabB2 à TabB1
    MOVQ  MM0,TabB1
    PADDB MM0,TabB2
    MOVQ  TabB1,MM0
    // addition des éléments de TabW2 à TabW1
    MOVQ  MM0,TabW1
    PADDW MM0,TabW2
    MOVQ  TabW1,MM0
    // addition des éléments de TabD2 à TabD1
    MOVQ  MM0,TabD1
    PADDD MM0,TabD2
    MOVQ  TabD1,MM0
  end;
end;

II-G. Chaînes courtes

Les chaînes courtes sont un tableau d'octets dont l'indice 0 donne la taille de la chaîne. Leur utilisation est identique à celle des tableaux, avec en plus la gestion de l'indice 0 comme longueur. Les instructions de chaîne sont adaptées au traitement de ce type de donnée.

 
Sélectionnez

Var
  Chaine:ShortString;
begin
  Asm // Chaine:='AAAAAAAA'
    PUSH EDI
    // Obtention de l'adresse du premier caractère
    LEA  EDI,CHAINE[1]
    MOV  ECX,8
    MOV  AL,'A'
    // Stockage des huit 'A'
    REP  STOSB
    // Mise à jour de la longueur de la chaîne
    MOV  Byte Ptr Chaine[0],8
    POP  EDI
  end;
end;

II-H. Enregistrements

L'assembleur intégré inclus aussi l'opérateur . identique à celui du pascal pour obtenir un membre d'un enregistrement. La concordance de taille est alors effectuée sur le membre en question et non le type global. Cela simplifie l'utilisation des enregistrements en assembleur l'offset de chaque membre étant ajouté par le compilateur.

 
Sélectionnez

Var
  Rect:TPoint;
begin
  Asm
    MOV EAX,RECT.X
    MOV ECX,RECT.Y
  end;
end;

Dans l'exemple ci-dessus, le compilateur va ajouter les offsets nécessaires pour atteindre les membres X et Y ( +0 pour X et +4 pour Y dans ce cas )
Le transtypage d'une adresse en variable de type enregistrement est aussi possible. C'est très utile dans le cas d'utilisation d'un index seul ou d'un tableau d'enregistrements.

 
Sélectionnez

Var
  TabRect:Array[0..9]Of TPoint;
  SommeX:Integer;
  SommeY:Integer;
begin
  Asm
    XOR  ECX,ECX
    XOR  EAX,EAX
    XOR  EDX,EDX
@L1:ADD  EAX,TabRect[ECX].TPoint.X
    ADD  EDX,TabRect[ECX].TPoint.Y
    ADD  ECX,Type TPoint
    CMP  ECX,Type TabRect
    JB @L1
    MOV  SommeX,EAX
    MOV  SommeY,EDX
  end;
end;

III. Ecrire une fonction en assembleur

III-A. Généralités

L'écriture d'une fonction ou d'une procédure en assembleur est effectuée en supprimant le begin de début de code et en le remplaçant par asm. Tout le code de la fonction doit être écrit en assembleur.

 
Sélectionnez

Function Somme(A,B :Integer) :Integer;
Asm
  ADD EAX,ADX
End;

Le fait de supprimer le begin, supprime toutes les copies de paramètres passés par valeur dont la taille est supérieure à 4 octets. Il faut considérer que le passage de tels paramètres est implicitement fait avec la directive var. ( voir III-E pour plus de détail en fonction des types de paramètres )

Note : le mot-clef Assembler n'existe que par soucis de compatibilité et ne doit pas être utilisé. De même le mot-clef Interrupt permettant l'écriture d'une procédure d'interruption n'existe plus depuis Delphi2.

III-B. Code d'entrée et de sortie

Lors de l'écriture d'une fonction en assembleur, Delphi gère automatiquement le cadre de pile pour l'accès aux paramètres et la gestion des variables locales. Ce cadre est optimisé en fonction des besoins, seules les instructions indispensables sont ajoutées au code.

L'entrée de la procédure ( Asm ) est codée par :

 
Sélectionnez

PUSH EBP     // seulement si la pile contient des paramètres…
MOV  EBP,ESP //  ou s'il existe des variables locales
SUB  ESP,XXX // Seulement dans le cas de variables locales

La sortie de la procédure ( End ) est codée par :

 
Sélectionnez

POP EBP // seulement si la pile contient des paramètres…
        //  ou s'il existe des variables locales
RET YYY // Codé dans tous les cas

Dans le cas où il n'y a pas de paramètres dans la pile ou dans le cas de la convention Cdecl, une simple instruction RET est générée.

Dans tous les cas le RET généré est un RET Near.

Du code supplémentaire est ajouté éventuellement est début et fin de fonction dans le cas d'utilisation de variables dynamiques comme les types String et Array Of -type-, ainsi que dans les constructeurs/destructeurs de classes.

III-C. Empilage des paramètres

L'empilage des paramètres est toujours effectué par mot entier (4 octets).
Si le paramètre à empiler est plus petit qu'un entier, il faut tout de même empiler 4 octets. Par exemple pour empiler la variable B de type byte il faut faire comme suit :

 
Sélectionnez

MOV  AL,B // Chargement de B
PUSH EAX  // Empilage d'un entier dans tous les cas

Dans le cas de paramètres empilés par valeur dans la pile mais dont la taille est supérieure à quatre octets, il faut commencer par empiler le poids fort. En effet, la pile étant construite en descendant, le poids fort sera en adresse la plus haute et le poids faible en adresse la plus basse.
Exemple pour V de type Int64 :

 
Sélectionnez

PUSH DWord ptr V[4]
PUSH DWord ptr V

III-D. Conventions d'appel

Il me semble utile de rappeler la signification des conventions de passage des paramètres. C'est très utile pour l'écriture de fonctions, mais surtout pour l'appel de fonctions, écrites en assembleur ou non.

III-D-1. Généralités

Toutes les fonctions utilisent la pile processeur pour le passage des paramètres. Cette pile est aussi utilisée pour la réservation de l'espace des variables locales à la fonction. L'ordre de passage dans la pile dépend de la convention utilisée. A la fin de la fonction, les paramètres et les variables locales sont supprimées de la pile et l'exécution du programme reprend à l'instruction suivant l'appel. La méthode de suppression des paramètres dépend aussi de la convention utilisée.

L'accès aux paramètres est effectué par le registre EBP. ESP n'est pas utilisé car il est automatiquement modifié par les instruction POP et PUSH, rendant inaccessibles les paramètres et variables locales. EBP est donc un registre très particulier dans les langages évolués. Il doit être sauvegardé dès le début de la fonction. Cette sauvegarde est automatiquement effectuée par Delphi à l'entrée de la fonction. L'instruction Begin ou Asm de début d'une fonction ajoute les lignes PUSH EBP et MOV ESP,EBP au code.

 
Sélectionnez

Procedure UneFonction(Param1,Param2 :Integer) ;Pascal ;
Var
  Entier1 :Integer;
  Real1 : Double;
Asm
  // code
End;

Au début du code utile de la fonction, l'aspect de la pile est le suivant :

 
Sélectionnez

         XXXXXX
EBP+12 : Param1  : Paramètre de la fonction
EBP+8  : Param2  : Paramètre de la fonction
EBP+4  : [EIP]   : Adresse de retour de la fonction
EBP>   : [EBP]   : Sauvegarde de EBP
EBP-4  : Entier1 : Variable locale de type integer
EBP-12 : Real1   : Variable locale de type double.

Le paramètre 1 sera alors accessible avec [EBP+12] La variable locale Entier1 sera accessible avec [EBP-4]

Important : il conseillé d'utiliser les identificateurs des paramètres au lieu de leur équivalent utilisant EBP, ceci rend la modification de la taille des variables et de leur place automatiques. Par exemple MOV EAX,Param1 est préférable à MOV EAX,[EBP+12]

Les paragraphes suivants décrivent toutes les conventions, en prenant pour exemple une fonction simple :

 
Sélectionnez

Function Diff(A,B :Integer) :Integer ;
Begin
  Result :=A-B;
End;

Le codage correspondant en assembleur sera donné pour chaque cas

III-D-2. Convention de type Pascal.

C'est la convention par défaut dans Delphi hors optimisations. Les paramètres sont empilés dans l'ordre de la fonction. La suppression des paramètres est à la charge de la fonction.

 
Sélectionnez

Function Diff(A,B :Integer) :Integer ;Pascal ;
Asm
  Mov EAX,A // A est ici [EBP+12]
  Sub EAX,B // B est ici [EBP+ 8]
End;

Avant l'appel :

 
Sélectionnez

ESP >   : XXXXXX

Après l'appel (juste avant l'exécution de la ligne Result :=)

 
Sélectionnez

        : XXXXXX
EBP+12  : A      : Paramètre de la fonction
EBP+8   : B      : Paramètre de la fonction
EBP+4   : [EIP]  : Adresse de retour de la fonction
ESP/EBP>: [EBP]  : Sauvegarde de EBP

Après l'exécution du End de la fonction

 
Sélectionnez

ESP >   : XXXXXX

Le code généré par le Begin (ou Asm ) est le suivant :

 
Sélectionnez

PUSH EBP
MOV  EBP,ESP

Le code généré par le End est le suivant :

 
Sélectionnez

POP  EBP
RET  8

On remarque que le RET 8 permet de supprimer les paramètres de la pile à la fin de la fonction.

III-D-3. Convention de type StdCall

C'est la convention par défaut des fonctions Windows et des dlls. Les paramètres sont empilés dans l'ordre inverse de la fonction. La suppression des paramètres est à la charge de la fonction.

 
Sélectionnez

Function Diff(A,B :Integer):Integer;StdCall;
Asm
  Mov EAX,A // A est ici [EBP+ 8]
  Sub EAX,B // B est ici [EBP+12]
End;

Avant l'appel :

 
Sélectionnez

ESP >   : XXXXXX

Après l'appel (juste avant l'exécution de la ligne Result :=)

 
Sélectionnez

ESP >   : XXXXXX :
EBP+12  : B      : Paramètre de la fonction
EBP+8   : A      : Paramètre de la fonction
EBP+4   : [EIP]  : Adresse de retour de la fonction
ESP/EBP>: [EBP]  : Sauvegarde de EBP

Après l'exécution du End de la fonction

 
Sélectionnez

ESP >   : XXXXXX

Le code généré par le Begin (ou Asm ) est le suivant :

 
Sélectionnez

PUSH EBP
MOV  EBP,ESP

Le code généré par le End est le suivant :

 
Sélectionnez

POP  EBP
RET  8

On remarque que le RET 8 permet de supprimer les paramètres de la pile à la fin de la fonction.

III-D-4. Convention de type C.

C'est la convention par défaut des fonctions écrites en C. Les paramètres sont empilés dans l'ordre inverse de la fonction. La suppression des paramètres est à la charge du programme appelant.

 
Sélectionnez

Function Diff(A,B :Integer):Integer;Cdecl;
Asm
  Mov EAX,A // A est ici [EBP+ 8]
  Sub EAX,B // B est ici [EBP+12]
End;

Avant l'appel :

 
Sélectionnez

ESP >   : XXXXXX

Après l'appel (juste avant l'exécution de la ligne Result :=)

 
Sélectionnez

        : XXXXXX :
EBP+12  : B      : Paramètre de la fonction
EBP+8   : A      : Paramètre de la fonction
EBP+4   : [EIP]  : Adresse de retour de la fonction
ESP/EBP>: [EBP]  : Sauvegarde de EBP

Après l'exécution du End de la fonction

 
Sélectionnez

        : XXXXXX :
        : B      : Paramètre non dépilé
ESP >   : A      : Paramètre non dépilé

Le code généré par le Begin (ou Asm ) est le suivant :

 
Sélectionnez

PUSH EBP
MOV  EBP,ESP

Le code généré par le End est le suivant :

 
Sélectionnez

POP  EBP
RET

Attention, dans ce cas c'est un simple RET qui est effectué, il faut penser à libérer les paramètres soi-même (ici avec un ADD ESP,8) après l'appel de la fonction.

III-D-5. Convention de type Register.

C'est la convention par défaut dans Delphi avec les optimisations. Les paramètres sont stockés dans la mesure du possible dans les registres. Les registres utilisés sont dans l'ordre EAX EDX et ECX. Si la fonction dispose de plus de trois paramètres, ils sont placés dans la pile comme pour la convention de type Pascal. De même les types réels ou int64 sont placés dans la pile. Les chaînes, les types structurés et les ensembles longs peuvent être en paramètre registre car ce sont des pointeurs vers un bloc du type correspondant.

Si il n'y a pas de paramètres dans la pile et pas de variables locales, EBP n'est pas sauvegardé.

 
Sélectionnez

Function Diff(A,B :Integer):Integer;Register;
Asm
  SUB EAX,EDX // A est ici EAX et B est EDX
End;

Avant l'appel :

 
Sélectionnez

ESP >   : XXXXXX :

Après l'appel (juste avant l'exécution de la ligne Result :=)

 
Sélectionnez

        : XXXXXX :
ESP >   :[EIP]   : Adresse de retour

Après l'exécution du End de la fonction

 
Sélectionnez

ESP >   : XXXXXX :

Dans ce cas il n'y a pas de code généré par Begin, car les deux paramètres sont placés dans EAX et EDX

Le code généré par le End est le suivant :

 
Sélectionnez

RET

III-E. Paramètres et résultat de la fonction

Quelle que soit la convention d'appel, le résultat d'une fonction est retourné de la même manière. Mais suivant le type de paramètre ou de résultat le codage est différent. Dans la suite nous allons décrire le codage d'une fonction " Somme " d'un certain nombre de types de paramètres, en retournant le résultat correspondant.
De plus un appel de cette fonction en assembleur sera également effectué afin de bien montrer dans chaque cas le code préparatoire à l'appel.
La convention utilisée ici est Register, Dans le cas des autres conventions la transformation des paramètres est la même. La différence est que les valeurs ordinales, ou assimilables à des valeurs ordinales, sont passés dans la pile.

Il est conseillé de mettre en tête de fonction comment sont passés les paramètres et comment le résultat est retourné. Cela facilite la maintenance des fonctions en assembleur.

Bien sûr il est possible de passer les paramètres sans respecter les normes. Mais ceci implique que la fonction et l'appel de la fonction soient écrits en assembleur. Delphi n'interprète jamais le passage des paramètres en fonction du code. Je conseille donc vivement de conserver le passage normal des paramètres.

III-E-1. Types ordinaux

Tous les types ordinaux, ainsi que les ensembles courts et les pointeurs. Le résultat est retourné dans AL pour tous les types occupant un octet. Dans AX pour les types occupant deux octets et dans EAX pour les types occupant 4 octets.

Fonction :

 
Sélectionnez

Function SommeInteger(A,B:Integer):Integer;
// A est dans EAX
// B est dans EDX
// Resultat dans EAX
Asm
  ADD EAX,EDX
end;

Appel :

 
Sélectionnez

Procedure AppelSomme;
Var v:Integer;
Begin
  Asm // V:=V+4
    MOV EAX,V
    MOV EDX,4
    CALL SommeInteger
    MOV V,EAX
  End;
End;

III-E-2. Types ensembles

Pour les ensembles courts (de 1 à 32 valeurs) le résultat est retourné dans AL, AX ou EAX suivant la taille de l'ensemble.

Pour les ensembles longs (33 à 256 valeurs), c'est le programme appelant qui doit réserver une zone mémoire pour le résultat de la fonction puis passer l'adresse de cette zone en tant que paramètre pointeur.

Fonction :

 
Sélectionnez

Type
  TListe  = 0..63;
  // Définition d'un type ensemble plus grand qu'un entier (sur 64 bits ici)
  TEnsLong=Set Of TListe;
Function SommeSetLong(Ens1,Ens2:TEnsLong):TEnsLong;Register;
// EAX : Adresse de Ens1
// EDX : Adresse de Ens2
// Resultat dans [ECX]
Asm
  PUSH EBX
  MOV  EBX,[EAX]
  OR   EBX,[EDX]
  MOV  [ECX],EBX
  MOV  EBX,[EAX+4]
  OR   EBX,[EDX+4]
  MOV  [ECX+4],EBX
  POP  EBX
End;

Important : l'emplacement du résultat de la fonction est réservé par l'appelant, celui-ci donnant un pointeur vers la zone réservée en tant que paramètre supplémentaire. ECX va donc contenir ici l'adresse de stockage du résultat.

Appel :

 
Sélectionnez

Procedure AppelSommeSetLongAsm;
// Les constantes Typées pour les paramètres constants
// sont le plus simple
Const
    CE1:TEnsLong=[0,2,4,6];
    CE2:TEnsLong=[4,20,63];
Var E2:TEnsLong;
Asm
  LEA  EAX,CE1 // Premier ensemble
  LEA  EDX,CE2 // Deuxième ensemble
  LEA  ECX,E2  // Adresse ou stocker le résultat
  CALL SommeSetLong
End;

Dans le code ci-dessus, remarquez l'utilisation des constantes typées pour les enregistrements constants. De même, le fait de déclarer une variable locale E2 permet de réserver la place pour le résultat de la fonction.

III-E-3. Types réels

Les paramètres réels sont passés par valeur dans la pile, bien que leur taille soit supérieure à 4 octets.
Les résultats réels sont retournés dans le registre du haut de la pile du coprocesseur. Attention, c'est au code appelant de dépiler le résultat :

Fonction :

 
Sélectionnez

Function SommeDouble(A,B:Double):Double;
// [EBP+$10] contient A ( sur 8 octets )
// [EBP+$08] contient B ( sur 8 octets )
// ST0 contiendra le résultat
Asm
  FLD  A
  FADD B
  WAIT
End;

Appel :

 
Sélectionnez

Procedure AppelSommeDouble;
Var A,R:Double;
Const B:Double=2.3;
Begin
  A:=1;
  Asm
    // R :=A+B
    PUSH DWORD PTR A[4] // Un double est sur 8 octets
    PUSH DWORD PTR A
    PUSH DWORD PTR B[4] // Un double est sur 8 octets
    PUSH DWORD PTR B
    CALL SommeDouble
    FSTP R              // Dépilage + Stockage du résultat

    // R :=A+2.3
    PUSH DWORD PTR A[4] // Un double est sur 8 octets
    PUSH DWORD PTR A
    PUSH $40026666      // Ici la constante réelle ( 2.3 ) est passée
    PUSH $66666666      // directement, mais il faut connaitre la valeur...
    CALL SommeDouble
    FSTP R              // Dépilage + Stockage du résultat
  End;
End;

III-E-4. Type Int64

Les paramètres de type Int64 sont passés par valeur dans la pile dans tous les cas.
Le résultat de type Int64 est retourné dans EDX:EAX. EDX contient les 32 bits de poids fort et EAX contient les 32 bits de poids faible.

Fonction :

 
Sélectionnez

Function SommeInt64(A,B:Int64):Int64;Register;
// [EBP+$10] Valeur de A
// [EBP+$08] Valeur de B
// Résultat : contenu dans EDX:EAX
Asm
  MOV EAX,DWORD PTR A
  ADD EAX,DWORD PTR B
  MOV EDX,DWORD PTR A[4]
  ADC EDX,DWORD PTR B[4]
End;

Appel :

 
Sélectionnez

Procedure AppelSommeInt64;
Var A:Int64;
    R:Int64;
Asm
  // Exemple ajout de $1234567890 à A et stockage dans R
  PUSH DWORD PTR A[4] // Il faut commencer par le point fort
  PUSH DWORD PTR A
  PUSH $00000012
  PUSH $34567890
  CALL SommeInt64
  MOV  DWORD PTR R,EAX
  MOV  DWORD PTR R[4],EDX
End;

III-E-5. Tableaux statiques

Les tableaux sont passés par adresse même dans le cas d'écriture de fonction par valeur. Il convient à la fonction appelante de faire une copie du tableau si la fonction est susceptible d'en modifier le contenu. L'emplacement du résultat doit être réservé par le programme appelant et il doit passer l'adresse de cette zone en tant que paramètre pointeur supplémentaire.

Fonction :

 
Sélectionnez

Type TTabInteger=Array[0..3]Of Integer;
Function SommeTableau(T1,T2:TTabInteger):TTabInteger;
// EAX adresse de T1
// EDX adresse de T2
// ECX adresse de stockage du résultat
Asm
  PUSH EDI
  PUSH EBX
  MOV  EDI,ECX
  XOR  ECX,ECX
@@L1:
  MOV  EBX,DWord Ptr T1[ECX*4]
  ADD  EBX,DWord Ptr T2[ECX*4]
  MOV  DWord Ptr [EDI+ECX*4],EBX
  INC  ECX
  CMP  ECX,4
  JB   @@L1
  POP  EBX
  POP  EDI
End;

Appel :

 
Sélectionnez

Procedure AppelSommeTableau;
Const
  Tab1:TTabInteger=(1,2,3,4);
  Tab2:TTabInteger=(4,3,2,1);
Var
  Res :TTabInteger;
Begin
  Asm
    LEA  EAX,Tab1
    LEA  EDX,Tab2
    LEA  ECX,Res
    Call SommeTableau
  End;
End;

III-E-6. Chaînes courtes

Les chaînes courtes sont traitées comme les tableaux, c'est-à-dire que les paramètres sont toujours passés par adresse, et que l'emplacement du résultat doit être réservé par le programme appelant.

Fonction : ( concaténation ici )

 
Sélectionnez

Function SommeChaine(A,B:ShortString):ShortString;
// EAX contient l'adresse de A
// EDX contient l'adresse de B
// ECX contient l'adresse de stockage du résultat
// Rem : aucun contrôle de longueur n'est effectué !!!
Asm
  PUSH EDI
  PUSH ESI
  MOV  EDI,ECX           // DI pointe sur le résultat
  MOV  CL,Byte ptr [EAX] // longeur de A
  ADD  CL,Byte ptr [EDX] // longeur de B
  MOV  Byte ptr[EDI],CL  // longeur totale
  INC  EDI               // Prise en compte longueur
  MOV  ESI,EAX           // ESI pointe sur A
  INC  ESI
  XOR  ECX,ECX           // ECX = longueur de A
  MOV  CL,Byte Ptr [EAX]
  REP  MOVSB             // Copie de A
  MOV  ESI,EDX           // ESI pointe sur B
  INC  ESI
  MOV  CL,Byte Ptr [EDX] // ECX = longueur de B
  REP  MOVSB             // Copie de B
  POP  EDI
  POP  ESI
End;

Appel :

 
Sélectionnez

Procedure AppelSommeChaine;
Const
  ST1:ShortString='1234';
  ST2:ShortString='5678';
Var
  Res:ShortString;
Begin
  Asm
    LEA  EAX,St1
    LEA  EDX,St2
    LEA  ECX,Res
    Call SommeChaine
  End;
  ShowMessage(Res);
End;

III-E-7. Enregistrements

Les enregistrements sont traités comme les tableaux, c'est-à-dire que les paramètres sont toujours passés par adresse, et que l'emplacement du résultat doit être réservé par le programme appelant.

Fonction :

 
Sélectionnez

Type TRec=Record
        rEntier1 : Integer;
        rEntier2 : Integer;
     End;
Function SommeRecord1(R1,R2:TRec):TRec;Register;
// EAX va contenir l'adresse de R1
// EDX va contenir l'adresse de R2
// ECX contient l'adresse de stockage du résultat
Const RecCST:TRec=(rEntier1:3;rEntier2:4);
Asm
  PUSH EBX
  MOV  EBX,TRec([EAX]).rEntier1
  ADD  EBX,TRec([EDX]).rEntier1
  ADD  EBX,RecCST.rEntier1
  MOV  TRec([ECX]).rEntier1,EBX
  MOV  EBX,TRec([EAX]).rEntier2
  ADD  EBX,TRec([EDX]).rEntier2
  ADD  EBX,RecCST.rEntier2
  MOV  TRec([ECX]).rEntier2,EBX
  POP  EBX
End;

Appel :

 
Sélectionnez

Procedure AppelSommeRecord;
Var A:TRec;
    R:TRec;
Const B:TRec=(rEntier1:1230;rEntier2:5670);
Asm
  LEA  EAX,A
  LEA  EDX,B
  LEA  ECX,R
  CALL SommeRecord1
End;

IV. Accéder aux objets

C'est la partie la plus délicate et dont l'application n'est pas possible dans tous les cas. Le programme doit réaliser le raisonnement du compilateur pour l'accès aux objets. La portée des membres d'un objet fait qu'il n'est pas toujours possible d'y accéder.

Nous supposerons dans la suite qu'il n'y a pas de problème de portée dans l'utilisation des membres de l'objet.

Dans ce chapitre, l'objet suivant sert d'exemple :

 
Sélectionnez

Type
  TMonObjet=Class
  Private
    FValeur : Integer;
    Fentier : Integer;
  Protected
    Constructor Create;
  Public
    Function    GetValeur :Integer;
    Function    GetValeur2:Integer;Virtual;
    Function    GetValeur3:Integer;Dynamic;
    Procedure   SetValeur(V:Integer);
    Class Function    GetInfo:Integer;
  Published
    Property Valeur:Integer read GetValeur Write SetValeur;
    Property Entier:Integer read FEntier   Write FEntier;
  End;

IV-A. Appel d'une méthode statique

L'appel d'une méthode statique d'un objet est identique à l'appel d'une fonction. Par contre toutes les méthodes d'objet ont un premier paramètre implicite qui est l'adresse de l'instance. Par défaut la convention register s'applique aux méthodes d'objet.

La fonction GetValeur est ici une méthode statique et retourne la valeur de la propriété Valeur :

 
Sélectionnez

MOV  EAX,MonObjet        // Paramètre implicite
CALL TMonObjet.GetValeur

IV-B. Appel d'une méthode virtuelle

L'appel d'une méthode virtuelle passe par une étape supplémentaire pour obtenir la " table des méthode virtuelles ".

La fonction GetValeur2 est ici une méthode virtuelle et retourne la valeur de la propriété Valeur :

 
Sélectionnez

MOV  EAX,MonObjet
MOV  EDX,[EAX] // Récupération de la VMT
CALL DWORD PTR [EDX + VMTOFFSET TMonObjet.GetValeur2]

IV-C. Appel d'une méthode dynamique

L'appel d'une méthode dynamique passe par une fonction spéciale appelant la méthode.

 
Sélectionnez

PUSH ESI
// EAX contient toujours l'adresse de l'instance
MOV  EAX,MonObjet
// L'indice de l'entrée DMT doit se trouver dans ESI
MOV  ESI, DMTINDEX TMonObjet.GetValeur3
// Appel de la méthode
CALL System.@CallDynaInst
POP ESI

IV-D. Appel d'une méthode de classe

Les méthodes de classes sont appelées comme une méthode statique mais n'ont pas de paramètre implicite contenant l'adresse de l'instance. L'appel s'effectue directement sur le type :

 
Sélectionnez

CALL TMonObjet.GetInfo

IV-E. Accès aux propriétés

Pour accéder aux propriétés d'un objet, vous devez connaître sa déclaration exacte.

Il faut connaître les paramètres read et write de la propriété. Si le paramètre est directement un membre interne, il faut accéder à ce membre. Si le paramètre passe par une fonction SetXXX ou GetXXX il faut appeler la méthode en question suivant la catégorie de la méthode (virtuelle ou non).

Dans l'exemple ci-dessus, la lecture de la propriété Valeur est effectuée par :

 
Sélectionnez

MOV  EAX,MonObjet
CALL TMonObjet.GetValeur
// EAX contient Valeur

Et l'écriture est effectuée par :

 
Sélectionnez

MOV  EAX,MonObjet        // Paramètre implicite
MOV  EDX,2244            // Paramètre explicite
CALL TMonObjet.SetValeur

Attention : ces méthodes peuvent être aussi virtuelles ou dynamiques, il faut dans ce cas appliquer la méthode correspondante.

Dans le cas d'une propriété associé directement à un membre interne :

 
Sélectionnez

Property Entier:Integer read FEntier Write FEntier;

La lecture de la propriété est effectuée comme suit :

 
Sélectionnez

MOV  EAX,MonObjet
MOV  EAX,[EAX].TMonObjet.Fentier

L'écriture de la propriété est effectuée comme suit :

 
Sélectionnez

MOV  EAX,MonObjet
MOV  [EAX].TMonObjet.Fentier,12345

Les méthodes GetXXX et SetXXX étant dans la plupart des cas privées ou protégées et les membres Fxxx privés, il sera dans la plupart des cas impossible d'accéder aux propriétés des objets de la VCL. Toutes les méthodes présentées ici sont données dans le cas d'écriture d'objets personnalisés dont les méthodes sont publiques ou dans le cas d'appel dans la même unité que celle contenant la définition des objets.

V. Création d'un objet.

Allez, juste pour le fun et afin d'avoir une classe dont le code est entièrement en assembleur, voici la méthode à suivre pour créer un objet en asm.

La suite du document présente la réalisation d'une classe TMonObjet2 descendante d'une classe TMonObjet. TMonObjet étant écrite en pascal ou en asm.

D'autre part, tout le code est donné en supposant que register est la convention d'appel. C'est le cas par défaut dans tous les projets Delphi.

La déclaration de TMonObjet2 est la suivante :

 
Sélectionnez

Type
  TMonObjet2=Class(TMonObjet)
  Private
    FEntier : Integer;
  Protected
    Function    GetEntier :Integer;
    Procedure   SetEntier(V:Integer);
  Public
    Constructor Create;
    Destructor Destroy;Override;
    Class  Function MethodeDeClasse:Integer;
  Published
    Property Entier:Integer read GetEntier Write SetEntier;
  End;

V-A. Le constructeur

Le constructeur reçoit deux paramètres implicites en plus de ceux donnés dans sa déclaration.
Le premier paramètre est l'adresse de la description de la classe, cette adresse est directement traitée dans le code ajouté en début de code par la directive asm. Ce code de début de constructeur crée l'instance de l'objet et retourne son adresse dans EAX. On admet alors que juste après le asm, EAX contient l'instance de l'objet.
Le deuxième paramètre est un booléen définissant si l'objet doit être créé. En effet, seul le premier appel du constructeur effectue la création de l'instance, tout appel ultérieur à un constructeur hérité n'effectue aucune création d'instance. Ce paramètre est transmis dans DL. Il est à usage interne de Delphi.
Si DL=0, la classe est déjà créée et EAX contient alors l'instance de cette classe.
Si DL=1, la classe doit être instanciée et EAX contient son descripteur.
Attention : ces deux paramètres doivent être conservés et restitués juste avant le end de fin de code, car le end ajoute du code de finalisation de création qui ne doit être exécuté qu'une seule fois.

De plus le constructeur doit à sa sortie laisser dans EAX l'adresse de l'instance créée.

Un constructeur simple, ne faisant que mettre à jour des variables privées ressemble à ceci :

 
Sélectionnez

constructor TMonObjet2.Create;
// EAX : implicite => contient l'adresse de l'instance
// DL : implicite => classe a créer
Asm // le code de création est géré par Delphi
  MOV [EAX].TMonObjet2.FEntier,12345
end;

En général tout constructeur appel son constructeur hérité, ceci est effectué en appelant directement le constructeur de la classe parente. Il faut penser à passer en paramètre les deux paramètres implicites en plus des paramètres explicites.
Le premier paramètre implicite ( EAX ) doit contenir l'instance de la classe.
Le deuxième paramètre implicite ( DL ) doit être à zéro, car l'instance est déjà créée et ne doit pas être créée à nouveau.
Le code du constructeur devient donc :

 
Sélectionnez

constructor TMonObjet2.Create;
// EAX : implicite => contient l'adresse de l'instance
// DL : implicite => classe a créer
Asm // le code de création est géré par Delphi
  PUSH EDX
  // Par définition le constructeur hérité va conserver EAX
  XOR EDX,EDX
  Call TMonObjet.Create
  MOV [EAX].TMonObjet2.FEntier,54321
  POP EDX
end;

A noter qu'il n'est pas utile de sauver EAX avec d'appeler la méthode héritée, car par définition le constructeur retourne l'adresse de l'instance. L'appel du constructeur hérité ne fait que conserver dans EAX la valeur reçue en entrée.

V-B. Le destructeur

Le fonctionnement du destructeur est sensiblement le même que celui du constructeur.
Le destructeur reçoit deux paramètres implicites en plus de ceux donnés dans sa déclaration.
Le premier paramètre est l'adresse de l'objet.
Le deuxième paramètre est un booléen définissant si l'objet doit être détruit. En effet, seul le premier appel du destructeur effectue la destruction de l'instance, tout appel ultérieur à un destructeur hérité n'effectue aucune destruction d'instance. Ce paramètre est transmis dans DL. Il est à usage interne de Delphi.
Si DL=0, l'instance ne doit pas être détruite
Si DL=1, l'instance doit être détruite
Attention : ces deux paramètres doivent être conservés et restitués juste avant le end de fin de code, car le end ajoute du code de finalisation de destruction qui ne doit être exécuté qu'une seule fois.

En général tout destructeur appelle son destructeur hérité, ceci est effectué en appelant directement le destructeur de la classe parente. Il faut penser à passer en paramètre les deux paramètres implicites en plus des paramètres explicites.
Le premier paramètre implicite ( EAX ) doit contenir l'instance de la classe.
Le deuxième paramètre implicite ( DL ) doit être à zéro, car l'instance ne doit être détruite qu'a la fin de l'appel du premier destructeur.

Le code du destructeur devient donc :

 
Sélectionnez

destructor TMonObjet2.Destroy;
// Le destructeur ne doit pas toucher à EDX
// EAX pointe sur l'instance en cours, et ne doit pas être modifié non plus.
Asm
  PUSH EDX
  MOV [EAX].TMonObjet2.FEntier,0
  // Appel du destructeur hérité
  XOR  EDX,EDX
  Call TMonObjet.Destroy
  POP  EDX
end;

V-C. Ecriture des méthodes.

Les méthodes doivent être écrites comme toutes les autres méthodes. Le fait qu'elle soit statique, virtuelle ou dynamique ou non ne change pas son code. Par contre toutes les méthodes ont un paramètre implicite qui est l'instance de l'objet.

Les méthodes d'accès aux propriétés doivent être écrites comme les autres.
Exemple de méthode GetXXX

 
Sélectionnez

function TMonObjet2.GetEntier: Integer;
// Paramètre implicite : EAX contient adresse de l'instance
Asm
  MOV EAX,[EAX].TMonObjet2.FEntier
end;

Exemple de méthode SetXXX

 
Sélectionnez

procedure TMonObjet2.SetEntier(V: Integer);
// Paramètre implicite : EAX contient adresse de l'instance
// Paramètre explicite : EDX contient la valeur de V.
Asm
  MOV [EAX].TMonObjet.FEntier,EDX
end;

Il est à noter, que si une méthode virtuelle appelle sa méthode héritée, il ne faut pas utiliser l'appel d'une méthode virtuelle comme expliqué dans le IV. Il faut appeler directement la méthode correspondante de la classe ancêtre comme on le ferait pour une méthode statique.

V-D. Ecriture d'une méthode de classe.

Les méthodes de classe n'ont pas de paramètres implicites. Elles sont écrites comme des procédures ou fonctions ordinaires.

Exemple :

 
Sélectionnez

class function TMonObjet2.MethodeDeClasse: Integer;
// Pas de paramètre implicite.
Asm
  MOV EAX,4444
end;

VI. Problèmes spécifiques à l'assembleur

Ce chapitre présente quelques problèmes de compilation ou d'exécution spécifiques à l'assembleur en ligne. L'erreur n'étant pas toujours immédiate dans ce cas.

VI-A. Fonction SizeOf()

Pour charger une taille d'objet dans un registre, on est tenté de faire :

 
Sélectionnez

MOV ECX, SizeOf(MonType)

Or ceci ne provoque pas d'erreur de compilation mais le code généré est faux. SizeOf utilisé en tant qu'opérande assembleur, retourne en permanence la valeur $32.
L'écriture correcte de la ligne ci-dessus est :

 
Sélectionnez

MOV ECX, Type MonType

VI-B. Erreur de codage due à OFFSET

L'opérateur OFFSET permet d'obtenir une adresse plutôt qu'une valeur. Suivant les cas le résultat obtenu est erroné. Et ceci sans erreur de compilation. OFFSET retourne une mauvaise valeur quand implicitement un registre est utilisé (EBP en général)

 
Sélectionnez

// Exemples de faux appel non signalés à la compilation
Procedure AppelSommeRecordErreur;
Var A:TRec;
    R:TRec;
Const B:TRec=(rEntier1:1230;rEntier2:5670);
Asm
  PUSH OFFSET A  // Cette ligne est une erreur car A n'a pas une adresse fixe
  PUSH OFFSET B  // Cette ligne est correcte car B est fixe (dans le code )
  LEA  EAX,R
  PUSH EAX
  Call SommeRecord2
End;

Il est donc conseillé d'utiliser deux lignes de code au lieu d'une et de se servir de l'instruction LEA :

 
Sélectionnez

LEA  EAX,B
PUSH EAX

Le fonctionnement est le même, mais la compilation est correcte dans tous les cas.

VI-C. Erreur due à un EBP implicite

EBP est ajouté implicitement dans les adressage des variables de la pile ou locales. Si déjà deux registres d'index sont utilisés, la ligne ne pourra être compilée.

 
Sélectionnez

// Exemple de faux adressage du à EBP
Procedure CalculSurTableauCarre;
Var T:Array[0..9,0..9]Of Integer;
Asm
  PUSH ESI
  PUSH EDI
  MOV  ESI,(10*4)*9 // ESI va balayer le premier indice
@@L1:
  MOV  EDI,(9*4) // EDI va balayre le deuxième indice
@@L2:
  MOV  EAX,ESI
  IMUL EAX,EDI
// La ligne suivante ne compile pas car T est [EBP-$00000190]
// ce qui ferait [ESI+EDI+EBP-$00000190], or seuls deux
// registres sont autorisés
  MOV  DWORD PTR T[ESI+EDI],EAX
  SUB  EDI,4
  JGE  @@L2
  SUB  ESI,10*4
  JGE  @@L1
  POP  EDI
  POP  ESI
End;

Attention : dans les versions antérieures de Delphi, cette ligne se compile sans erreur, mais ne fonctionne pas vu qu'il n'est pas possible d'utiliser trois index. Ceci peut provoquer des résultats très bizarres.

VI-D. Erreur d'adresse dans des sous-procédures

Delphi n'utilise pas les fonctions d'entrée ENTER et LEAVE pour les imbrications de procédures. Il peut en résulter que l'adressage des variables des niveaux supérieurs est incorrect. Tant que les instructions PUSH et POP ne sont pas utilisées, l'adressage est correct.

 
Sélectionnez

// Exemple d'erreur d'adressage du à PUSH
// avant l'appel d'une sous-procédure
Procedure ErreurVariableSousProcedure;
Var i,j,k:Integer;
  Procedure SousProcedure;
  Var l:Integer;
  Asm
    MOV l,1
    MOV EAX,l
    ADD j,EAX   // Erreur de compilation
                // Est compilé en ADD [EBP-$04],EAX
                // EBP pointant sur la cadre de SousProcedure
  End;
Asm
  PUSH EAX
  MOV  i,1
  MOV  j,1
  MOV  k,1
  CALL SousProcedure
  POP  EAX
End;

Conclusion

L'assembleur en ligne reste tout de même accessoire lors de la programmation Objet, mais dans le cas d'applications graphiques, son utilisation peut apporter un gain de performance important. Reste que l'on peut aussi l'utiliser par plaisir… Afin de se rappeler le temps où nous n'avions pas le choix.

Version PDF de cet article :
Miroir 1 : Version PDF
Dans le cas où le miroir 1 ne fonctionne pas :
Miroir 2 : Version PDF

Merci à Bloon pour la correction orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2003 Bruno Guérangé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.