Section courante

A propos

Section administrative du site

Problèmes de contrôle

Le Turbo Pascal a mise en oeuvre très particulière sur la mise en oeuvre de son contrôle de programme, comme par exemple dans les conventions d'appel et les procédures de sortie.

Conventions d'appel

Les paramètres sont transférés aux procédures et aux fonctions via la pile. Avant d'appeler une procédure ou une fonction, les paramètres sont poussés sur la pile dans leur ordre de déclaration. Avant de revenir, la procédure ou la fonction supprime tous les paramètres de la pile. Le code squelette au niveau de son assemblage d'une procédure ou d'un appel de fonction ressemble à ceci :

  1. PUSH Paraml
  2. PUSH Param2
  3.  :
  4. PUSH ParamX
  5. CALL ProcOuFunc 

Les paramètres sont transmis soit par référence, soit par valeur. Lorsqu'un paramètre est passé par référence, un pointeur pointant vers l'emplacement d'entreposage réel est poussé sur la pile. Lorsqu'un paramètre est passé par valeur, la valeur réelle est poussée sur la pile.

Paramètres variables

Les paramètres variables (paramètres var) sont toujours passés par référence - un pointeur pointant vers l'emplacement d'entreposage réel.

Paramètres de valeur

Les paramètres de valeur sont passés par valeur ou par référence en fonction du type et de la taille du paramètre. En général, si le paramètre value occupe 1, 2 ou 4 octets, la valeur est poussée directement sur la pile. Sinon, un pointeur vers la valeur est poussé et la procédure ou la fonction copie ensuite la valeur dans un emplacement d'entreposage local. Le 8086 ne prend pas en charge les instructions PUSH et POP de taille octet, donc les paramètres de taille octet sont toujours transférés sur la pile sous forme de mots. L'octet de poids faible du mot contient la valeur et l'octet de poids fort est inutilisé (et indéfini). Un type ou paramètre entier est passé sous forme d'octet, de mot ou de mot double, en utilisant le même format qu'une variable de type entier. (Pour les mots doubles, le mot de poids fort est poussé avant le mot de poids faible de sorte que le mot de poids faible se termine à l'adresse la plus basse.) Un paramètre Char est passé sous la forme d'un octet non signé. Un paramètre Boolean est passé sous forme d'octet avec la valeur 0 ou 1. Un paramètre de type énuméré est passé sous la forme d'un octet non signé si l'énumération a 256 valeurs ou moins; sinon, il est passé comme un mot non signé. Un paramètre de type à virgule flottante (Real, Single, Double, Extended et Comp) est passé sous la forme 4, 6, 8 ou 10 octets sur la pile. Il s'agit d'une exception à la règle selon laquelle seules les valeurs de 1, 2 et 4 octets sont transmises directement sur la pile. Un paramètre de type pointeur est passé sous forme de deux mots (un double mot). La partie de segment est poussée avant la partie de déplacement de sorte que la partie de déplacement se termine à l'adresse la plus basse. Un paramètre de type chaîne de caractères est passé en tant que pointeur vers la valeur. Pour un paramètre de type d'ensemble, si les limites du type d'élément de l'ensemble sont toutes deux comprises entre 0 et 7, l'ensemble est passé sous forme d'octet. Si les limites sont toutes les deux comprises entre 0 et 15, l'ensemble est passé sous forme de mot. Sinon, l'ensemble est passé en tant que pointeur vers un ensemble décompressé occupant 32 octets. Les tableaux et les enregistrements avec 1, 2 ou 4 octets sont transmis directement à la pile. Les autres tableaux et enregistrements sont transmis en tant que pointeurs vers la valeur.

Paramètres ouverts

Les paramètres de chaîne de caractères ouverte sont passés en poussant d'abord un pointeur sur la chaîne de caractères, puis en poussant un mot contenant l'attribut size (longueur maximale) de la chaîne de caractères. Les paramètres de tableau ouvert sont passés en poussant d'abord un pointeur vers le tableau, puis en poussant un mot contenant le nombre d'éléments dans le tableau moins un. Lors de l'utilisation de l'assembleur intégré, la valeur renvoyée par la fonction High standard pour un paramètre ouvert est accessible en chargeant le mot juste en dessous du paramètre ouvert. Dans cet exemple, la procédure FillString, remplissant une chaîne de caractères à sa longueur maximale avec un caractère donné, le montre :

  1. Procedure FillString(Var Str:OpenString;Chr:Char);Assembler;
  2. ASM
  3.  LES DI,Str          { ES:DI = @Str }
  4.  MOV CX,Str.Word[-2] { CX = High(Str) }
  5.  MOV AL,CL
  6.  CLD
  7.  STOSB               { Fixe Str[0] }
  8.  MOV AL,Chr
  9.  REP STOSB           { Fixe Str[1..High] }
  10. END;

Résultats de la fonction

Les résultats des fonctions de type ordinal sont retournés dans les registres du microprocesseur : les octets sont retournés dans AL, les mots sont retournés dans AX et les mots doubles sont retournés dans DX:AX (mot de poids fort dans DX, mot de poids faible dans AX). Les résultats des fonctions de type réel (type de données Real) sont renvoyés dans les registres DX:BX:AX (mot de poids fort en DX, mot du milieu en BX, mot de poids faible en AX).

Les résultats des fonctions de type 80x87 (type Single, Double, Extended et Comp) sont renvoyés dans le registre de haut de pile du coprocesseur 80x87 (ST(0)). Les résultats des fonctions de type pointeur sont renvoyés dans DX:AX (partie segment en DX, partie déplacement dans AX). Pour un résultat de fonction de type chaîne de caractères, l'appelant pousse un pointeur vers un emplacement d'entreposage temporaire avant de pousser des paramètres, et la fonction renvoie une valeur de chaîne de caractères dans cet emplacement temporaire. La fonction ne doit pas supprimer le pointeur.

Appel NEAR et FAR

La famille de processeurs 80x86 prend en charge deux types d'instructions d'appel et de retour : court (NEAR) et éloigné (FAR). Les instructions courts transfèrent le contrôle vers un autre emplacement dans le même segment de code, et les instructions éloignées permettent un changement de segment de code. Une instruction CALL NEAR pousse une adresse de retour de 16 bits (déplacement uniquement) sur la pile, et une instruction CALL FAR pousse une adresse de retour de 32 bits (segment et déplacement). Les instructions RET correspondantes affichent uniquement un déplacement ou à la fois un déplacement et un segment. Le Turbo Pascal sélectionne automatiquement le modèle d'appel correct en fonction de la déclaration de la procédure. Les procédures déclarées dans la section interface d'une unité sont éloignées - elles peuvent être appelées à partir d'autres unités. Les procédures déclarées dans un programme ou dans la section de mise en oeuvre d'une unité sont courtes - elles ne peuvent être appelées qu'à partir de ce programme ou unité. Pour certaines raisons spécifiques, une procédure peut être exigée pour être loin. Par exemple, si une procédure ou une fonction doit être affectée à une variable procédurale, elle doit être éloignée. La directive du compilateur $F est utilisée pour remplacer la sélection automatique du modèle d'appel du compilateur. Les procédures et fonctions compilées dans l'état {$F+} sont toujours éloignées; dans l'état {$F-}, le Turbo Pascal sélectionne automatiquement le modèle correct. L'état par défaut est {$F-}.

Procédures et fonctions imbriquées

Une procédure ou une fonction est dite imbriquée lorsqu'elle est déclarée dans une autre procédure ou fonction. Par défaut, les procédures et fonctions imbriquées utilisent toujours le modèle d'appel proche, car elles ne sont visibles que dans une procédure ou une fonction spécifique dans le même segment de code. Dans une application en recouvrement, cependant, une directive {$F+} est généralement utilisée pour forcer toutes les procédures et fonctions à être éloignées, y compris celles étant imbriquées. Lors de l'appel d'une procédure ou d'une fonction imbriquée, le compilateur génère une instruction PUSH BP juste avant le CALL, passant en fait le BP de l'appelant comme paramètre supplémentaire. Une fois que la procédure appelée a établi son propre BP, le BP de l'appelant est accessible sous forme de mot mémorisé à [BP+4], ou à [BP+6] si la procédure est éloignée. En utilisant ce lien en [BP+4] ou [BP+6], la procédure appelée peut accéder aux variables locales dans la trame de pile de l'appelant. Si l'appelant lui-même est également une procédure imbriquée, il a également un lien à [BP+4] ou [BP+6], et ainsi de suite. L'exemple suivant montre comment accéder aux variables locales à partir d'une instruction en ligne dans une procédure imbriquée :

  1. Procedure A; Near;
  2. Var
  3.  IntA:Integer;
  4.  
  5.  Procedure B;Far;
  6.  Var
  7.   IntB:Integer;
  8.   
  9.   Procedure C; Near;
  10.   Var
  11.    IntC:Integer;
  12.   Begin
  13.    ASM
  14.     MOV AX,l
  15.     MOV IntC,AX                  { IntC := 1 }
  16.     MOV BX,[BP+4]                { Cadre de pile de B }
  17.     MOV SS:[BX+OFFSET IntB],AX   { IntB := 1 }
  18.     MOV BX,[BP+4]                { Cadre de pile de B }
  19.     MOV BX,SS:[BX+6]             { Cadre de pile de A }
  20.     MOV SS:[BX+OFFSET IntA],AX   { IntA := 1 } 
  21.    END;
  22.   End;
  23.   
  24.  Begin 
  25.   C 
  26.  End;
  27.  
  28. Begin 
  29.  B
  30. End; 

Conventions d'appel de méthode

Les méthodes utilisent les mêmes conventions d'appel que les procédures et fonctions ordinaires, sauf que chaque méthode a un paramètre implicite supplémentaire, Self, correspondant à un paramètre var du même type que le type d'objet de la méthode. Le paramètre Self est toujours passé en dernier paramètre et prend toujours la forme d'un pointeur 32 bits vers l'instance via laquelle la méthode est appelée. Par exemple, étant donné une variable PP de type PPoint, l'appel PP^.MoveTo(10, 20) est codé comme suit :

  1. MOV AX,10              ; Charge 10 dans AX
  2. PUSH AX                ; Passe un paramètre PX
  3. MOV AX,20              ; Charge 20 dans AX
  4. PUSH AX                ; Passe un paramètre PY
  5. LES DI,PP              ; Charge PP dans ES:DI
  6. PUSH ES                ; Passe le paramètre Self
  7. PUSH DI
  8. MOV DI,ES:[DI+6]       ; Récupérer le déplacement VMT du champ VMT
  9. CALL DWORD PTR [DI+20] ; Appel l'entrée VMT pour MoveTo

Au retour, une méthode doit supprimer le paramètre Self de la pile, tout comme elle doit supprimer tous les paramètres normaux. Les méthodes utilisent toujours le modèle d'appel FAR, quel que soit le paramètre de la directive du compilateur {$F}.

Appels de méthode virtuelle

Pour appeler une méthode virtuelle, le compilateur génère du code récupérant l'adresse VMT du champ VMT dans l'objet, puis appelle via l'emplacement associé à la méthode. Par exemple, étant donné une variable PP de type Point, l'appel PP^.Show génère le code suivant :

  1. LES DI,PP              ; Charge PP dans ES:DI
  2. PUSH ES                ; Passe le paramètre Self
  3. PUSH DI
  4. MOV DI,ES:[DI+6]       ; Récupérer le déplacement VMT du champ VMT
  5. CALL DWORD PTR [DI+12] ; Appel l'entrée VMT pour Show

Les règles de compatibilité de type des types d'objet permettent à PP de pointer sur un point ou un TCircle, ou sur tout autre descendant de TPoint. Et si vous examinez les VMT, vous verrez que pour un TPoint, l'entrée au déplacement 12 dans le VMT pointe vers TPoint.Show; alors que pour un TCircle, il pointe vers TCircle.Show. Par conséquent, selon le type d'exécution réel de PP, l'instruction CALL appelle TPoint.Show ou TCircle.Show, ou la méthode Show de tout autre descendant de TPoint. Si Show avait été une méthode statique, le compilateur l'aurait généré pour l'appel à PP^ :

  1. LES DI,PP        ; Charge PP dans ES:DI
  2. PUSH ES          ; Passe un paramètre Self
  3. PUSH DI
  4. CALL TPoint.Show ; Appel directe TPoint.Show

Ici, peu importe ce vers quoi PP pointe, le code appelle toujours la méthode TPoint.Show.

Appels de méthode dynamiques

La distribution d'un appel de méthode dynamique est un peu plus compliquée et prend du temps que la distribution d'un appel de méthode virtuelle. Au lieu d'utiliser une instruction CALL pour appeler via un pointeur de méthode à un déplacement statique dans le VMT, le DMT du type d'objet et les DMT parents doivent être analysés pour trouver l'occurrence la plus élevée d'un index de méthode dynamique particulier, puis un appel doit être effectué via le pointeur de méthode correspondant. Ce processus implique beaucoup plus d'instructions que ce qui peut être codé en ligne, de sorte que la bibliothèque d'exécution Turbo Pascal (RTL) contient une routine de support de répartition étant utilisée lors des appels de méthode dynamiques. Si la méthode Show du type précédent TPoint avait été déclarée comme méthode dynamique (avec un index de méthode dynamique de 200), l'appel PP^.Show, où PP est de type Point, générerait le code suivant :

  1. LES DI,PP        ; Charge le PP dans ES:DI
  2. PUSH ES          ; Passe au paramètre Self
  3. PUSH DI
  4. MOV DI,ES:[DI+6] ; Récupérer le déplacement VMT du champ VMT
  5. MOV AX,200       ; Index de méthode dynamique de chargement dans AX
  6. CALL Dispatch    ; Appeler la routine RTL pour distribuer l'appel

Le répartiteur RTL prend d'abord le déplacement DMT du VMT pointé par le registre DI. Ensuite, en utilisant le champ d'index mis en cache du DMT, le répartiteur vérifie si l'index de méthode dynamique de la méthode appelée est le même que le dernier ayant été appelé. Si tel est le cas, il transfère immédiatement le contrôle à la méthode, en passant indirectement par le pointeur de méthode entreposé au déplacement donné par le champ de déplacement d'entrée mis en cache. Si l'index dynamique de la méthode appelée n'est pas le même que celui entreposé dans le cache, le répartiteur analyse le DMT et les DMT parents (en suivant les liens parents dans les DMT) jusqu'à ce qu'il localise une entrée avec la dynamique donnée index de méthode. L'index et le déplacement du pointeur de méthode correspondant sont ensuite entreposés dans les champs de cache du DMT et le contrôle est transféré à la méthode. Si, pour une raison quelconque, le répartiteur ne parvient pas à trouver une entrée avec l'index de méthode dynamique donné, indiquant que les DMT ont été détruits d'une manière ou d'une autre, il met fin à l'application avec une erreur d'exécution 210. Malgré la mise en cache et une routine de support de distribution RTL hautement optimisée, la distribution d'un appel de méthode dynamique prend beaucoup plus de temps qu'un appel de méthode virtuelle. Cependant, lorsque les actions effectuées par les méthodes dynamiques elles-mêmes prennent beaucoup de temps, la quantité d'espace économisée en utilisant les DMT peut l'emporter sur cette pénalité.

Constructeurs et destructeurs

Les constructeurs et les destructeurs utilisent les mêmes conventions d'appel que les autres méthodes, sauf qu'un paramètre supplémentaire de la taille d'un mot, appelé le paramètre VMT, est passé sur la pile juste avant le paramètre Self. Pour les constructeurs, le paramètre VMT contient le déplacement VMT à entreposer dans le champ Self de VMT pour initialiser Self. Lorsqu'un constructeur est appelé pour allouer un objet dynamique à l'aide de la syntaxe étendue de la procédure standard New, un pointeur NIL est passé dans le paramètre Self. Le constructeur alloue un nouvel objet dynamique, dont l'adresse est renvoyée à l'appelant dans DX:AX lorsque le constructeur revient. Si le constructeur ne peut pas allouer l'objet, un pointeur NIL est renvoyé dans DX:AX. Enfin, lorsqu'un constructeur est appelé à l'aide d'un identificateur de méthode qualifié (c'est-à-dire un identificateur de type d'objet, suivi d'un point et d'un identificateur de méthode), une valeur de zéro est passée dans le paramètre VMT. Cela indique au constructeur qu'il ne doit pas initialiser le champ VMT de Self. Pour les destructeurs, un 0 dans le paramètre VMT indique un appel normal et une valeur différente de zéro indique que le destructeur a été appelé à l'aide de la syntaxe étendue de la procédure standard Dispose. Cette situation amène le destructeur à désallouer Self juste avant de revenir (la taille de Self est trouvée en regardant le premier mot du VMT de Self).

Code d'entrée et de sortie

Chaque procédure et fonction Pascal commence et se termine par un code d'entrée et de sortie standard. C'est le code d'entrée standard :

  1. PUSH BP            ; Sauvegarde BP
  2. MOV BP,SP          ; Configure le cadre de la pile
  3. SUB SP,LocalSize   ; Allouer des locaux (le cas échéant)

Le LocalSize est la taille des variables locales. L'instruction SUB n'est présente que si LocalSize n'est pas 0. Si le modèle d'appel de la procédure est court, les paramètres commencent à BP+4; s'il est éloigné, alors ils commencent à BP+6. Voici le code de sortie standard :

  1. MOV SP,BP      ; Désallouer les locaux (le cas échéant)
  2. POP BP         ; Restaurer la BP
  3. RET ParamSize  ; Supprimer les paramètres et retourner

Le ParamSize est la taille des paramètres. L'instruction RET est un retour court ou éloigné, selon le modèle d'appel de la routine.

Conventions d'enregistrement des registres

Les procédures et fonctions doivent conserver les registres BP, SP, SS et DS. Tous les autres registres peuvent être modifiés.

Procédures de sortie

En installant une procédure de sortie, vous pouvez prendre le contrôle du processus de terminaison d'un programme. Cette situation est utile lorsque vous voulez vous assurer que des actions spécifiques sont effectuées avant la fin d'un programme; un exemple typique est la mise à jour et la fermeture de fichiers. La variable de pointeur ExitProc vous permet d'installer une procédure de sortie. La procédure de sortie est toujours appelée dans le cadre de l'arrêt d'un programme, qu'il s'agisse d'une terminaison normale, d'une terminaison via un appel à Halt ou d'une terminaison due à une erreur d'exécution. Une procédure de sortie ne prend aucun paramètre et doit être compilée avec une directive de procédure FAR pour la forcer à utiliser le modèle d'appel à distance. Lorsqu'elle est correctement mise en oeuvre, une procédure de sortie devient en fait partie d'une chaîne de caractères de procédures de sortie. Cette chaîne de caractères permet aux unités ainsi qu'aux programmes d'installer des procédures de sortie. Certaines unités installent une procédure de sortie dans le cadre de leur code d'initialisation, puis s'appuient sur cette procédure spécifique pour être appelée pour nettoyer après l'unité. La fermeture des fichiers est un exemple. Les procédures sur la chaîne de caractères de sortie sont exécutées dans l'ordre inverse de l'installation. Cette situation garantit que le code de sortie d'une unité n'est pas exécuté avant le code de sortie de toutes les unités qui en dépendent. Pour conserver la chaîne de caractères de sortie intacte, vous devez enregistrer le contenu actuel de ExitProc avant de le changer à l'adresse de votre propre procédure de sortie. En outre, la première instruction de votre procédure de sortie doit réinstaller la valeur enregistrée d'ExitProc. Le programme suivant illustre une méthode squelette de mise en oeuvre d'une procédure de sortie :

  1. Program TestSortie;
  2. Var
  3.  ExitSave:Pointer;
  4.  
  5. Procedure MaSortie;Far;Begin
  6.  ExitProc := ExitSave; { Toujours restaurés l'ancien premier vecteur }
  7. End;
  8.  
  9. BEGIN
  10.  ExitSave := ExitProc;
  11.  ExitProc := @MaSortie;
  12. END.

À l'entrée, le programme enregistre le contenu d'ExitProc dans ExitSave, puis installe la procédure de sortie MaSortie. Après avoir été appelé dans le cadre du processus de terminaison, la première chose que fait MaSortie est de réinstaller la procédure de sortie précédente. La routine de terminaison dans la bibliothèque d'exécution continue d'appeler les procédures de sortie jusqu'à ce que ExitProc devienne NIL. Pour éviter les boucles infinies, ExitProc est défini sur NIL avant chaque appel, de sorte que la procédure de sortie suivante n'est appelée que si le processus de sortie actuel attribue une adresse à ExitProc. Si une erreur se produit dans une procédure de sortie, elle ne sera plus appelée. Une procédure de sortie peut connaître la cause de l'arrêt en examinant la variable entière ExitCode et la variable de pointeur ErrorAddr. En cas de terminaison normale, ExitCode vaut zéro et ErrorAddr est NIL. En cas de terminaison via un appel à Halt, ExitCode contient la valeur transmise à Halt et ErrorAddr est NIL. Enfin, en cas de résiliation due à une erreur d'exécution, ExitCode contient le code d'erreur et ErrorAddr contient l'adresse de l'instruction en erreur.

La dernière procédure de sortie (celle installée par la bibliothèque d'exécution) ferme les fichiers d'entrée et de sortie. Si ErrorAddr n'est pas nul, il génère un message d'erreur d'exécution. Si vous souhaitez présenter vous-même les messages d'erreur d'exécution, installez une procédure de sortie examinant ErrorAddr et renvoie un message s'il n'est pas NIL. De plus, avant de retourner, assurez-vous de définir ErrorAddr sur NIL, afin que l'erreur ne soit pas signalée à nouveau par d'autres procédures de sortie. Une fois que la bibliothèque d'exécution a appelé toutes les procédures de sortie, elle retourne au DOS, en transmettant la valeur entreposée dans ExitCode comme code de retour.

Gestion des interruptions

La bibliothèque d'exécution Turbo Pascal et le code généré par le compilateur sont totalement interruptibles. En outre, la plupart de la bibliothèque d'exécution est réentrante, ce qui vous permet d'écrire des routines de service d'interruption dans Turbo Pascal.

Rédaction de procédures d'interruption

Déclarez les procédures d'interruption avec la directive d'interruption. Chaque procédure d'interruption doit spécifier l'entête de procédure suivant (ou un sous-ensemble de celui-ci, comme expliqué plus loin) :

  1. Procedure IntHandler(Flags,ES,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP:Word);Interrupt;Begin
  2.  { ... }
  3. End;

Comme vous pouvez le voir, tous les registres sont passés sous forme de pseudoparamètres afin que vous puissiez les utiliser et les modifier dans votre code. Vous pouvez omettre certains ou tous les paramètres, en commençant par Flags et en vous déplaçant vers BP. C'est une erreur de déclarer plus de paramètres que ceux répertoriés dans l'exemple précédent, ou d'omettre un paramètre spécifique sans également omettre ceux le précédant (bien qu'aucune erreur ne soit signalée). Par exemple le code suivant est invalide :

  1. Procedure IntHandler(DI,ES,BP:Word); { Entête invalide }

mais le code suivant est valide :

  1. Procedure IntHandler(SI,DI,DS,ES,BP:Word); { Entête valide }

A l'entrée, une procédure d'interruption sauvegarde automatiquement tous les registres (quel que soit l'entête de la procédure) et initialise le registre DS :

  1. PUSH AX
  2. PUSH BX
  3. PUSH CX
  4. PUSH DX
  5. PUSH SI
  6. PUSH DI
  7. PUSH DS
  8. PUSH ES
  9. PUSH BP
  10. MOV BP,SP
  11. SUB SP,LocalSize
  12. MOV AX,SEG DATA
  13. MOV DS,AX 

Notez l'absence d'une instruction STI pour activer des interruptions supplémentaires. Vous devez le coder vous-même (si nécessaire) à l'aide d'une instruction en ligne. Le code de sortie restaure les registres et exécute une instruction d'interruption-retour :

  1. MOV SP, BP
  2. POP BP
  3. POP ES
  4. POP DS
  5. POP DI
  6. POP SI
  7. POP DX
  8. POP CX
  9. POP BX
  10. POP AX
  11. IRET 

Une procédure d'interruption peut modifier ses paramètres. La modification des paramètres déclarés modifiera le registre correspondant au retour du gestionnaire d'interruption. Cette situation peut être utile lorsque vous utilisez un gestionnaire d'interruption comme service utilisateur, un peu comme les services DOS du INT 21h. Les procédures d'interruption gérant les interruptions générées par le matériel ne doivent utiliser aucune des routines d'entrée et de sortie ou d'allocation de mémoire dynamique de Turbo Pascal, car elles ne sont pas réentrantes. De même, aucune fonction DOS ne peut être utilisée car DOS n'est pas réentrant.



Dernière mise à jour : Dimanche, le 15 novembre 2020