Section courante

A propos

Section administrative du site

Introduction

Le Turbo Pascal 3 était très efficace pour l'époque et beaucoup de personne ont effectué du reverse engineering pour mieux comprendre comment il fonctionnait. Voici donc les analyses de certains personnes afin de mieux comprendre le Turbo Pascal 3 en interne et sa génération du code.

Structure du compilateur

Les compilateurs se composent généralement des groupes fonctionnels suivants :

Le Turbo Pascal 3 n'est pas votre compilateur habituel ... L'analyseur est entrecoupé de parties du générateur de code, et il n'y a pas d'optimiseur. La plupart des compilateurs ont besoin de plusieurs passages (PASS) pour faire leur travail, mais le Turbo Pascal 3 est un compilateur à une seule passage (plus rapide). Heureusement, les langages de programmation de type Pascal sont conçus avec une compilation simple en un seul passage à l'esprit. Tous les symboles doivent être définis avant d'être utilisés. Le compilateur peut facilement déterminer le type d'une constante sans regarder vers l'avant :

Analyse lexicale

La tâche de l'analyse lexicale est de lire le code source à partir de la mémoire ou d'un fichier d'inclusion, d'éliminer les commentaires et de reconnaître les directives, symboles et mots-clefs du compilateur. Il est appelé par l'analyseur. A chaque appel un élément de langage (mot-clef, symbole, constante ...) est lu. La position de départ est enregistrée. Si une erreur est détectée, l'éditeur démarre et le curseur pointe vers cette position.

Analyse des structures de programme

La tâche de cette partie du compilateur est d'analyser la structure du programme à compiler et de vérifier la syntaxe. Comme la plupart des compilateurs Pascal, le Turbo Pascal 3 utilise un analyseur de descente récursif. La génération de code est incluse dans l'analyseur. La compilation des structures de programme est assez simple. Habituellement, la syntaxe est décrite sous forme de Backus-Naur ou par un «schéma ferroviaire». À titre d'exemple, l'instruction IF sera couverte.

IF cond THEN stat1 {ELSE stat2}

Après avoir lu un élément, l'analyseur prend la piste applicable. S'il n'y en a pas, la syntaxe est incorrecte et une erreur est signalée. Il est possible d'avoir un analyseur généré automatiquement par un soi-disant compiler-compiler, si la forme Backus-Naur de la syntaxe est donnée. Malheureusement, cela n'aide pas beaucoup: les parties vraiment difficiles d'un compilateur - la génération de code et l'optimisation - doivent encore être écrites manuellement. Comment la déclaration IF est-elle convertie ? (La section correspondante dans le compilateur est au déplacement 6C12h). La procédure d'instruction lit un IF et appelle la procédure IF. Tout d'abord, la condition - en fait une expression arithmétique de type booléen - est évaluée. Cela se fait en appelant la procédure d'expression. L'expression est lue jusqu'à ce qu'un symbole THEN soit trouvé. Cela met fin à l'expression, dont le type est booléen. La procédure IF insère ici un saut conditionnel à la fin de l'instruction 1. Le déplacement est inséré plus tard - il n'est pas encore connu. Si l'expression a été terminée par autre chose qu'un THEN, une erreur est signalée. Maintenant, la première instruction (stat1) est convertie. En fait, il s'agit d'un appel récursif de la procédure d'instruction (c'est pourquoi il s'agit d'un analyseur de descente récursif). Veuillez noter que la définition de la syntaxe est également récursive ! En raison de possibles instructions IF imbriquées, les variables de la procédure IF sont enregistrées sur la pile. Après cette déclaration, un ELSE peut suivre. Si c'est le cas, un saut à la fin de stat2 est émis et le saut depuis le début de stat1 est corrigé, puis la deuxième instruction est convertie et le deuxième saut corrigé. Le code assembleur 8086 produit ressemble à ceci (IF .. THEN) :

  1. cond
  2.     JNZ l1
  3.     JMP l2
  4. l1: stat1
  5. l2: ...

Tandis qu'un code code assembleur 8086 produit ressemble à ceci pour un IF .. THEN .. ELSE :

  1. cond
  2.      JNZ l1
  3.      JMP l2
  4.  l1: stat1
  5.      JMP l3
  6.      l2: stat2
  7.  l3: ...

Le saut en longueur au début n'est pas toujours nécessaire. Malheureusement, le compilateur ne peut pas prédire la durée de l'instruction. Pour améliorer cela, le saut devrait être remplacé par un saut court et le code suivant déplacé, ce qui compliquerait un peu le compilateur. Toutes les autres structures de programme sont converties de la même manière.

Analyse des expressions arithmétiques

L'évaluation des expressions est un peu plus complexe, car la priorité des opérations doit être prise en compte. La solution dans Turbo Pascal 3 est cependant assez simple (code commençant à 7A70h). Les expressions sont généralement converties en notation polonaise inversée (soit la méthode Infix to Postfix). Il existe 5 groupes d'opérations :

On pourrait le convertie par la structure de programme suivante :

  1. Procedure atom;Begin
  2.  Case operation of
  3.   CONST:lecture_constant;
  4.   VAR  :lecture_variable;         { indexé -> récursive }
  5.   '('  :lecture_expression;       { récursive }
  6.         ')' doit_suivre;
  7.   func :lectures_parametres;      { récursive }
  8.         appel_emettre_fonction;
  9.   TYPE :'(' doit_suivre;          { type conversion, exemmple. Integer(TRUE) }
  10.         lecture_expression;       { récursive }
  11.        ')' doit_suivre;
  12.         convertir_type -> type_rechercher
  13.   Else syntax_error;
  14.  End;
  15. End;
  16.  
  17. Procedure neg; { négation - }
  18. Var negflag:Boolean;
  19. Begin
  20.   negflag:=(operation=neg);
  21.   atom;
  22.   If negflag Then emettre_la_negation;
  23. End;
  24.  
  25. Procedure NOT; { NOT }
  26. Var 
  27.  notflag:Boolean;
  28. Begin
  29.  notflag:=(op=NOT);
  30.  neg;
  31.  IF notflag THEN emettre NOT;
  32. End;
  33.  
  34. PROCEDURE mult_level; { multiplication ... }
  35. Var
  36.  mult_op:type_operation;
  37. Begin
  38.   NOT;
  39.   While op in mult_ops do Begin
  40.     sauve_le_resultat;
  41.     mult_op:=op;
  42.     NOT;
  43.     emettre_operation(mult_op);
  44.   End;
  45. End;
  46.  
  47. Procedure add_level; { addition ... }
  48. Var 
  49.  add_op:type_operation;
  50. Begin
  51.   mult_level;
  52.   While operation In add_ops do Begin
  53.     save the result;
  54.     add_op:=operation;
  55.     mult_level;
  56.     emettre_operation(add_op);
  57.   End;
  58. End;
  59.  
  60. Procedure expression; { comparisons, IN }
  61. Var 
  62.  cmp_op:type_operation;
  63. Begin
  64.   add_level;
  65.   If operation in cmp_ops THEN BEGIN
  66.     save the result;
  67.     cmp_op:=op;
  68.     add_level;
  69.     emit_operation(cmp_op);
  70.   END;
  71. End;

Le traitement des multiplication : Le contenu de a doit être empilé, car le registre AX est nécessaire pour la multiplication. Lequel est reconnu en définissant le drapeau push_ax. Si le code suivant utilise le registre AX (détruisant son contenu), il doit émettre PUSH AX. Enfin, si cela s'est produit, le registre doit être restauré par POP CX. Le code produit est plutôt simple. En convertissant l'expression en «b * c + a», le code pourrait être produit :

  1. MOV  AX,b
  2. IMUL c
  3. ADD  AX,a

Pendant l'évaluation, la vérification de type et la conversion de type (Integer ← Real ...) sont également effectuées. L'ensemble d'instructions 8086/8088 est souvent mal utilisé. L'expression a := a + 1 donne ce code (l'instruction INC a serait une meilleure solution) :

  1. MOV  AX,a
  2. ADD  AX,1
  3. MOV  a,AX

Les expressions représentent généralement la majeure partie du code produit, leur conversion est donc très importante.

Optimisation

L'objectif de l'optimisation du code est de réduire la taille et/ou le temps d'exécution du code produit. Il est généralement impossible de trouver une solution optimale, car un compromis espace-temps doit être fait. Le Turbo Pascal 3 n'a pas d'optimiseur. Cependant, pour améliorer l'efficacité de vos programmes par des optimisations manuelles ou par des optimiseurs complémentaires, il est bon de savoir comment fonctionnent les optimisations courantes. Les optimisations peuvent être locales ou globales : elles peuvent couvrir une seule instruction ou un programme entier. L'optimisation globale est beaucoup plus difficile et peut causer des problèmes. Les appels GOTO et les appels de fonction ou de procédure peuvent empêcher l'optimiseur de fonctionner efficacement. Les effets secondaires peuvent provoquer des erreurs difficiles à trouver. Un exemple :

  1. Function mafonction:Integer;Begin
  2.  side_effect:=side_effect+1;
  3.  mafonction:=5;
  4. End;
  5.  
  6. BEGIN
  7.  { ... }
  8.  a:=side_effect+mafonction+side_effect;
  9.  { ... }
  10. END.

La séquence d'évaluation et donc le résultat dépendent du compilateur utilisé. Les variables ne restent pas nécessairement constantes entre les affectations. Considère ceci :

  1. wait_int:=FALSE;
  2. Repeat Until wait_int;

Ainsi, il peut attendre une instruction d'interruption pour définir un drapeau. Un compilateur optimisant convertirait cela en une boucle sans fin ... Les compilateurs C modernes utilisent le mot clef volatile pour éviter ce genre de situation.

Utilisation des variables de registre

De nombreuses opérations de chargement et d'entreposage peuvent être éliminées en utilisant des variables de registre. Sur le 8086/8088, c'est assez difficile, car il y a peu de registres, souvent avec des utilisations spéciales. Par exemple, dans les sous-expressions suivantes :

  1. c:=(a+b)*d;
  2. e:=g-(a+b);

La sous-expression (a + b) peut être utilisée deux fois. Les expressions de la forme a[i]:= a[i]+1 sont également une bonne cible pour les optimisations.

Indexation des tableaux

Les références avec des indices constants (a[5]) ou des indices avec un déplacement constant (a[i+1]) peuvent être optimisées. L'indexation des tableaux dans les boucles peut également être considérablement améliorée.

Pliage constant

Les programmes peuvent être plus lisibles si les expressions de constantes peuvent être écrites sous une forme symbolique. Le compilateur peut évaluer ces expressions au moment de la compilation. Les versions ultérieures du compilateur le font.

Réduction de la force

Cette expression signifie remplacer les opérations par des équivalents moins lourds en puissance, par exemple x * 0.2 au lieu de x/5 (les multiplications sont plus rapides que les divisions, prenant plus de cycle d'horloge de microprocesseur).

Optimisation des boucles

Prenons par exemple, la boucle suivante :

  1. FOR i:=1 TO 100 DO dest[i]:=a+b;

La sous-expression a+b peut être évaluée en dehors de la boucle, car a et b ne changent pas dans la boucle.

Élimination du code mort

Prenons par exemple, le code suivant n'est jamais exécuté :

  1. CONST debug=FALSE;
  2.  
  3. IF debug THEN writeln('Debug');

L'instruction IF peut être omise - la condition n'est jamais remplie. La même chose peut être faite avec des procédures n'étant jamais utilisées. Il existe des optimiseurs éliminant toutes les procédures inutilisées de la bibliothèque d'exécution des programmes traduits par Turbo Pascal. Les versions ultérieures du compilateur le font.

Évaluation des expressions booléennes

Prenons par le code suivant :

  1. IF (a=5) AND (b=6) THEN 

Il peut être changé selon le format suivant :

  1. IF (a=5) THEN
  2.  IF (b=6) THEN ...

La même chose peut être faite avec les opérateurs OR et NOT. Ne vous attendez jamais à ce que les expressions booléennes soient exécutées complètement ! Les versions ultérieures du compilateur le font.

Alignement variable

Les variables dans le segment de données et sur la pile doivent être alignées sur des déplacements réguliers pour améliorer les performances sur les IBM PC en 16 bits.

Génération de code

Le générateur de code a la tâche difficile de convertir les éléments reconnus par l'analyseur en code exécutable. S'il devient difficile de dire si le code a été généré par un programmeur humain ou par un compilateur, c'est en effet un bon code... Ne vous attendez pas trop à cela de la part de Turbo Pascal 3. Dans les sections suivantes, le code produit par Turbo Pascal 3 sera expliqué.

  1. Program
  2. ;biblioth+èque d'exécution, sinon fichier de chaîne
  3. CALL  initmem       ;définir des segments
  4. W     mainflag      ;voir le code source
  5. W     turbocs,turbods
  6. W     cssize,dssize
  7. w     heapsize,maxhpsz
  8. w     maxfile,stdinsz,stdoutsz
  9. MOV   BP,SP         ;cadre de pile
  10. CALL  uncrunch      ;étendre les recouvrements
  11. W     link,*+2
  12.         ; partie de définition
  13. ; partie de programme = programme principal
  14. XOR   AX,AX         ;Arr+?t
  15. CALL  progend

Partie de définition

La partie définition peut contenir du code, elle doit donc être ignorée par :

  1. JMP   l1
  2. l1:

Constantes structurées

Les constantes structurées sont entreposées dans le même format que les variables normales.

Recouvrements

L'espace nécessaire pour les recouvrements n'est pas entreposé dans le fichier .COM. Il est libéré par la procédure de décroissance. Cette situation signifie qu'il remonter au code suivant. Le code est exécuté au début de l'exécution du programme et après le chargement d'une procédure de recouvrement.

  1. CALL  rdover        ;lire le fichier de recouvrement
  2. W     $ffff         ;procédure de recouvrement maintenant en mémoire = invalide
  3. B     'over.001'    ;nom du fichier de recouvrement

Voici le code dans la section lire à partir du fichier de recouvrement :

  1. CALL  uncrunch      ;développer le recouvrement
  2. W     link,*+2      ;lien pour les procédures et les fonctions de recouvrement de décroissance
  3. W     link,*        ;pour décroiser

Définitions à l'avance

Pour les définitions à l'avance, un saut vers la définition finale est produit. Le déplacement est inséré lors de la définition réelle.

  1. JMP defined_proc

Procédures externes

Le code lu à partir d'un fichier externe n'est pas modifié.

Définitions de procédure

Les variables locales des procédures et des fonctions sont toujours entreposées sur la pile. Cette situation signifie que seules les procédures actives occupent de l'espace sur la pile. Cela permet également les appels récursifs. Le transfert des paramètres et l'allocation de l'espace de pile peuvent être assez compliqués, ralentissant ainsi les appels de procédure. Pour chaque procédure, une structure de données appelée cadre de pile ou enregistrement d'activation est construite sur la pile. Le pointeur vers le cadre de pile est toujours entreposé dans le registre BP (le 8086/8088 ne peut pas utiliser le pointeur de pile SP comme registre d'index). La structure du cadre de pile est la suivante :

Position Description
BP+. Résultat de la fonction (espace alloué par l'appelant)
BP+. Premier paramètre
BP+4 Dernier paramètre
BP+2 Adresse de retour
BP+0 Pointeur vers le cadre de pile de l'appelant
BP-2 Nouveau cadre de pile
BP-4 Variables locales
BP-. Haut de la pile

Le code d'une entrée de procédure standard ressemble à ceci :

  1. PUSH  BP            ; enregistrer l'ancien pointeur
  2. MOV   BP,SP         ; définir un nouveau pointeur
  3. PUSH  BP            ; enregistrer un nouveau pointeur (pour l'affichage)
  4. definition_part     ; constantes, procédures locales
  5. SUB   SP,#size      ; allouer de l'espace pour les variables locales
  6.                     ; 1..2 octets: DEC SP
  7. program part        ; la procédure réelle
  8. MOV   SP,BP         ; oublier les variables locales
  9. POP   BP            ; restaurer l'ancien pointeur
  10. RET   prmsize       ; retourne, supprime les paramètres de la pile
  11.                     ; pas de paramètres: RET

La manière dont les résultats des fonctions sont transmis dépend de leur type. Les scalaires (entier ...) sont renvoyés dans le registre AX, pour les résultats booléens, les drapeaux sont définis avec OR AX, AX. Les vrais sont de toute façon sur la pile. Les chaînes de caractères doivent être déplacées de manière à n'occuper que leur longueur effective :

  1. MOV   DX,#pos_on_stack
  2. MOV   CL,#max_len
  3. MOV   SP,BP
  4. POP   BP
  5. JMP   retstr        ; la fin normale est omise

Malheureusement, les choses ne sont pas aussi simples. Considérez les définitions de procédure imbriquées :

  1. Procedure level1;
  2. Var
  3.  i:Integer;
  4.  
  5.  Procedure level2;Begin
  6.   i:=0;
  7.   level2;
  8.  End;
  9.  
  10. Begin
  11.  level2;
  12. End;

La procédure interne level2 utilise une variable locale de level1, mais s'appelle également elle-même de manière récursive. Le déplacement de pile de i dépend de l'ordre d'appel. Le Turbo Pascal 3 utilise un soi-disant affichage pour résoudre ce problème. L'affichage contient des pointeurs vers les cadres de pile des procédures d'appel. Chaque procédure ajoute également son propre pointeur à l'affichage. L'affichage est une extension du cadre de la pile.

Position Description
BP+0 Ancien pointeur
BP-2 Afficher la procédure la plus externe
BP-2 Afficher la procédure la plus externe
BP-. Afficher
BP-. Afficher la procédure en cours
BP-. Variables locales
BP-. Haut de la pile

Ceci est maintenu par le code suivant :

  1. PUSH  BP            ; enregistrer l'ancien pointeur
  2. MOV   AX,SP         ; d+?finir un nouveau pointeur - garder BP
  3. PUSH  [BP-nest*2]   ; affichage de la construction
  4.  ..                 ; une fois pour chaque niveau d'imbrication
  5. MOV   BP,AX         ; d+?finir un nouveau pointeur
  6. PUSH  BP            ; ajouter son propre pointeur +? afficher
  7. definition_part

Les microprocesseurs plus récents (80186, 80286,...) ont des commandes spéciales pour ces opérations (ENTER, LEAVE). Veuillez noter que le référencement des variables via l'affichage est plus lent que les références normales. Si la vitesse est importante, n'imbriquez pas les définitions de procédure.

Structures de programme

Partie de programme

  1. statements
  2. l1:  JMP l2        ; sauter à la fin
  3. POP   AX      ; GOTO, EXIT: nettoyer la pile
  4. JMP   l1
  5. l2:

Les GOTO et EXIT ne sont pas vraiment aussi simples. Parfois, les variables de pile (FOR, WITH) doivent être supprimées, ce qui est fait à la fin de la procédure.

Déclaration

Si la directive d'interruption utilisateur est définie, un INT 3 est émis pour chaque instruction. Cela appelle une routine vérifiant les interruptions de l'utilisateur. Cette fonctionnalité peut être mal utilisée pour tracer un programme ou pour profiler son temps d'exécution. Si cela n'est utilisé nulle part dans le programme, vous pouvez également insérer des points d'arrêt sous forme d'instructions INLINE pour le débogage avec DEBUG.

WHILE

La boucle WHILE est résolue de la façon suivante :

  1. l1:  condition           ;évaluer la condition
  2. J..   l2            ;:condition remplie
  3. JMP   l3
  4. l2:  statement
  5. JMP   l1            ;réessayer
  6. l3:                     ;fin de boucle

REPEAT

  1. l1:  statement
  2. condition           ; évaluer la condition
  3. J..   l2            ; condition remplie: fin
  4. JMP   l1            ; non satisfait: répéter
  5. l2:

FOR

Le compteur (entreposé sur pile) et la variable de contrôle sont indépendants : les affectations à la variable de contrôle ne modifient pas le nombre d'exécutions de boucle.

  1. ; valeur de départ -> AX
  2. PUSH  AX
  3. ; valeur finale -> AX
  4.   POP   CX
  5. XCHG  CX,AX
  6. SUB   CX,AX         ; calculer la différence
  7. JGE   l1            ; (DOWNTO: JNG)
  8. JMP   l3            ; ne pas exécuter
  9. l1:  INC CX              ; (DEC CX)
  10.  
  11. l2:  PUSH CX             ; compteur de sauvegarde
  12.  
  13. POP   CX            ; compteur de restauration
  14.   DEC   CX            ;(INC CX)
  15. JZ    l3            ;0: terminé
  16. INC   loop_var      ;(DEC) variable de contrôle de mise +à jour
  17. JMP   l2            ;boucle
  18. l3:                         ;fin

CASE

Prenons pour exemple le code suivant :

  1. CASE .. OF
  2.   2,5 : .. ;
  3.   7..9: .. ;
  4.   ELSE  .. ;
  5. END;

Nous aurons un code générer ressemblant à ceci :

  1.  ;évaluer la sélection
  2. CMP   AX,#2         ;comparer
  3. JZ    ok1           ;:oui
  4. CMP   AX,#5
  5. JZ    ok1           ;:oui
  6. JMP   test2         ; essayez le cas suivant
  7. ok1:           ;correcte - exécuter
  8. JMP   endcase       ;aller à la fin
  9. test2:
  10. CMP AX,#7           ;vérifier le sous-intervalle
  11. JL    test2no       ;:non
  12. CMP   AX,#9
  13. JLE   ok2           ;:oui
  14. test2no:
  15. JMP else            ;non: exécute la partie ELSE
  16. ok2:             ;correcte - exécuter
  17. JMP   endcase
  18. else:              ; partie ELSE
  19. endcase:

Les instructions CASE compliquées peuvent dépasser l'intervalle des sauts courts. Dans ce cas, ce qu'on appelle des sauts longs par hanches sont émis :

GOTO

Un saut est émis. Si le GOTO laisse un bloc WITH ou FOR, la pile doit être nettoyée. Ceci est reconnu et corrigé à la fin de la partie programme.

WITH

Le compilateur a une pile WITH interne. Les pointeurs pour les WITH indexés sont entreposés sur la pile :

    http://localhost/CODER/TPASCAL7/case.htm
  1. ; définir le pointeur sur la variable
  2. PUSH ES             ; entreposer le pointeur sur la pile
  3. PUSH DI
  4. statement
  5. ADD   SP,#4         ; supprimer le pointeur de la pile

Si l'adresse est connue au moment de la compilation, cela n'est pas nécessaire.

Appels de procédure

Si la directive de compilation $K+ est définie, un contrôle de pile est exécuté :

  1. MOV   CX,#space_needed
  2. CALL  xchkstk

Ensuite, les paramètres sont évalués et transmis. Paramètre normal :

  1. ; évaluer l'expression
  2. ; contrôle de plage en option
  3. PUSH  DX       ; pointeur
  4. PUSH  AX       ; scalaire et pointeur

Pour les paramètres de chaîne de caractères :

  1. MOV CL, #max_length ; la chaîne est étendue à la longueur maximale
  2. CALL xstrparm       ; -> sur la pile comme une variable locale

Pour les paramètres d'ensemble (SET) :

  1. MOV   CX,#crunch    ; régler le paramètre de crunch:
  2.                     ;  lo = nombre d'octets
  3.                     ;  hi = nombre d'octets vides au début
  4. CALL  xsetparm      ; adapte le SET

Dans le cas des réels, le paramètre est déjà sur la pile.

Variable structurée

  1. ; Fixe le pointeur vers une variable
  2. MOV   CX,#size
  3. MOV   xblkparm      ; copier la variable sur la pile

Paramètres VAR placer le pointeur sur la pile.

  1. ; définir le pointeur sur la variable
  2. PUSH ES
  3. PUSH DI

Pour les procédures de recouvrements, le code suivant doit être inséré :

  1. MOV   AX,#longueur/256
  2. MOV   DX,#position_dans_le_fichier_de_recouvrement / 256

Ensuite, la procédure est appelée par le code suivant :

  1. CALL  procedure

Appel de fonction

L'espace de pile est alloué pour le résultat (SUB SP, #space_needed), tout le reste est identique. Au retour, le résultat est sur pile (réel, chaîne de caractères) ou en AX (entier, scalaire).

Appel de procédures et de fonctions standard

Les procédures standard comme la lecture et l'écriture peuvent avoir n'importe quel nombre de paramètres de n'importe quel type. Cela signifie qu'une grande flexibilité est nécessaire. Ce problème est résolu dans Turbo Pascal 3 de manière très efficace : la table de procédure standard ne contient que l'adresse de la routine de conversion correspondante. Cette routine émet le code pour lire les paramètres (certains d'entre eux passés dans les registres!) Et pour appeler la bibliothèque d'exécution. Certaines fonctions (Swap, Hi, Lo) n'appellent pas de procédure mais créent du code en ligne à la place.

Affectations et expressions

Scalaire/pointeur : les registres ES et DI sont enregistrés, si nécessaire. Cela n'est fait que si l'expression ne consiste pas en une constante ou une simple variable. Variable normale :

  1. ; définir le pointeur sur la variable
  2. PUSH ES ; enregistrer le pointeur vers la variable de destination
  3. PUSH DI
  4. ; évaluer l'expression
  5. ; conversion de type
  6. ; entreposer le résultat dans la variable de destination

Dans le cas de variable structurée :

  1. ; pointeur vers la deuxième variable
  2. MOV CX, #size ; pointeur vers la variable de destination sur la pile
  3. CALL xmovevar ; copier la variable

Les conversions de type son traiter de la manière suivante :

  1. CALL xintreal; Integer -> Real
  2. MOV AH,AL; Char -> String: char -> second octet
  3. MOV AL,01; longueur: 1 caractère
  4. PUSH AX; pousser comme une chaîne de caractères
  5. CALL xstrch; Chaîne -> Caractère

Expressions

Les opérations arithmétiques pour les scalaires sont émises par ecalc. Pour chaque opération, un bloc de paramètres (commençant à à l'adresse 973E) contrôle la génération du code.

Expression Assembleur
expr1 - 5 SUB AX,#5
expr1 - var SUB AX,var
expr1 - expr2 XCHG CX, AX (premier résultat en CX, deuxième en AX)
SUB AX, CX

Les expressions du type a:=a+1 ne sont pas bien converties. a:=succ(a) est une meilleure solution, mais n'est pas optimal.

Définir des expressions

Les ensembles sont entreposés sous une forme compressée et doivent être développés à leur taille maximale (32 octets) pour effectuer des opérations sur les ensembles. En raison de cet ensemble, les opérations ont tendance à être lentes. Les constructeurs d'ensemble sont gérés de manière inefficace. [5, var1..var2] est convertie comme ceci :

  1. CALL  sldempty      ;entrepose l'ensemble vide sur la pile
  2. MOV   AX,#5         ;expression = 5
  3. CALL  setincl       ;inclure l'+élément dans l'ensemble
  4. MOV   AX,var1       ;première intervalle d'expression
  5. PUSH  AX            ;enregistrer
  6. MOV   AX,var2       ;deuxième sous-plage d'expression
  7. CALL  setinrng      ;inclure la sous-plage dans l'ensemble

Si les paramètres sont variables, tout va bien. Pour les ensembles constants, c'est désastreux.

Références de variables

MEM / MEMW

  1. expression: segment
  2. PUSH  AX
  3. expression: offset
  4. XCHG  DI,AX         ;pointeur -> ES:DI
  5. POP   ES

Utilisez ABSOLUTE pour les variables avec une adresse constante.

Indexation avec WITH

Dans un bloc WITH, tous les noms de variables doivent être recherchés d'abord dans les portées des enregistrements actifs, puis dans la table de symboles standard. Cela peut prendre un certain temps. Si le déplacement de base n'est pas connu au moment de la compilation (WITH rec[var] DO), un pointeur WITH doit être calculé et entreposé sur la pile, sinon cela se fait au moment de la compilation.

Indexation de tableau, indexation de chaîne de caractères

Si nécessaire, ES et DI doivent être sauvegardés avant l'évaluation. Un code différent est produit en fonction de l'index. Index constant : L'index est vérifié au moment de la compilation, multiplié par la taille de l'élément et ajouté au déplacement de base, aucun code n'est émis.

Index de variable avec contrôle de l'intervalle :

  1. SUB AX,#lower_bound ; peut être DEC AX / rien
  2. MOV CX,#upper_bound+1
  3. CALL xindchk ; vérifier l'index

Index de variable sans contrôle d'intervalle : La soustraction de la limite inférieure peut être omise, elle est multipliée par la taille de l'élément puis soustraite du déplacement de base. L'index est multiplié par la taille de l'élément. Ceci est optimisé pour certaines tailles d'élément importantes :

  1. ; pas de code; taille = 1
  2. SHL AX, 1; taille = 2
  3. SHL AX, 1; taille = 4
  4. SHL AX, 1
  5. SHL AX, 1; taille = 6
  6. MOV CX,AX
  7. SHL AX,1
  8. ADD   AX,CX
  9. MOV CX, # taille; autres tailles d'éléments
  10. MUL CX

L'index est ensuite entreposé dans DI :

  1. XCHG  DI,AX

ou ajouté à l'index existant :

  1. ADD   DI,AX

Indexation des enregistrements

C'est très simple: le déplacement de la variable d'enregistrement est ajouté au déplacement de mémoire de la variable.

Indexation du pointeur

Les pointeurs sont chargés avec LES DI, pointer_var.

Utilisation des modes d'adressage

La procédure einstr émet une commande utilisant le mode d'adressage correct. Si nécessaire, un préfixe de segment (CS: ou ES:) est inséré. non indexé, pas sur la pile :

  1. MOV   AX,var

Si indexé :

  1. MOV   AX,[DI]        ; Pas de déplacement
  2. MOV   AX,[DI]offs8   ; Déplacement court (-128..127)
  3. MOV   AX,[DI]offs16  ; Déplacement long  (0..65535)

Dans le cas d'une pile et variables locales :

  1. MOV AX,[BP]offs    ;non indexé
  2. MOV AX,[BP+DI]offs ;indexé

Dans le cas d'une pile et de variables locales des appelants :

  1. MOV   BX,[BP]-lev*2  ; lecture du pointeur d'affichage
  2. SS:                  ; indexé par BX - préfixe nécessaire
  3. MOV   AX,[BX]offs    ; non indexé
  4. MOV   AX,[BX+DI]offs ; ou indexé

Calculer le pointeur vers la variable

Le déplacement est lu dans le registre DI :

  1. MOV DI, #offset; non indexé
  2. ADD DI, #offset; indexé: offset court ou long
  3. LEA DI, [BP]offs; pile: charger l'adresse effective

Ensuite, le segment est remis sur la pile :

  1. PUSH  CS/DS/ES/SS

Lire une variable

Pour lire une variable scalaire :

  1. MOV AX, var ; entier, non indexé, pas sur la pile
  2. MOV AL, var ; octet, non indexé, pas sur la pile
  3. MOV AX,..   ; autre entier (émis par einstr)
  4. MOV AL,..   ; autre octet

Pour les variables d'un octet, l'instruction XOR AH, AH est inséré - le Turbo Pascal 3 utilise toujours des variables entières en interne. Pour lire des variables de pointeurs :

  1. LES   AX,ptr_var    ;AX = déplacement
  2. MOV   DX,ES         ;DX = segment

Pour lire des variables réels :

  1. ; définir le pointeur sur la variable
  2. CALL  xldreal

Pour lire des variables de SET :

  1. ; Fixe le pointeur à variable
  2. MOV   CX,#set_crunch
  3. CALL  xldset

Pour lire des variables de chaîne de caractères STRING :

  1. ; Définir le pointeur sur la variable
  2. CALL strload

Entreposage la variable

Scalaire : si la directive {$R+} est défini, la vérification de l'intervalle est effectuée :

  1. MOV   CX,#lower_bound
  2. MOV   DX,#upper_bound
  3. CALL  xrngchk

Si la variable n'est ni indexée ni sur la pile, il y a à nouveau une format d'entier courte :

  1. MOV var, AX ; entier
  2. MOV var, AL ; octet

Sinon, le MOV correct est émis par un einstr. Le pointeur est :

  1. MOV   dest,AX    ; déplacement
  2. MOV   dest+2,DX  ; segment

Pour un nombre réel :

  1. ; définir le pointeur sur la variable
  2. CALL  xstoreal

Pour une chaîne de caractères :

  1. ; définir le pointeur sur la variable
  2. MOV   CL,#max_length
  3. CALL  strstore

Pour un ensemble (SET) :

  1. ; Définir le pointeur sur la variable
  2. MOV   CX,#set_crunch
  3. CALL  setsto

Table des symboles

Le gestionnaire de table de symboles doit insérer et rechercher des symboles de tout type. La difficulté est que les définitions peuvent être de n'importe quelle complexité et que les noms peuvent avoir n'importe quelle longueur. La structure mise en oeuvre dans Turbo Pascal 3 représente étroitement la définition et la séquence de référence, ce qui facilite la navigation à travers les types complexes. Les symboles sont recherchés en commençant par la définition la plus récente. Pour les nouvelles définitions, le bloc actuel (limité par la borne définie au début d'une définition de procédure) et la table de mots-clefs sont recherchés pour les définitions en double. Ainsi, il est possible de remplacer les anciennes définitions. À la fin d'une procédure, les variables locales peuvent être supprimées de la table des symboles. Cela réduit l'utilisation de la mémoire et le temps de recherche.

Structure d'entrée de la table des symboles

La table de symboles symtab est entreposée dans le segment de pile (la pile réelle n'a pas besoin de beaucoup d'espace) et s'agrandit. Les entrées ont cette structure de base :

Position Description
off Entrée suivante
... ...
off-2 Mot de balise = type d'entrée
off-4 Longueur du nom
off-5 Nom
off-. Entrée
0 Déplacement vers l'entrée suivante

La table des symboles est toujours recherchée en arrière, en regardant d'abord les entrées les plus récentes. Une recherche linéaire est utilisée. Des exemples d'entrées de table de symboles peuvent être trouvés dans les tables du compilateur (à partir de l'adresse 9277). Voici les mots de balises :

Valeur Nom Description
0100 Label 1: imbrication des procédures (pour éviter les sauts dans ou en dehors des procédures)
2: 0 = ok, FF = pas encore défini
4: déplacement
0200 Constant 1: type de constante
3: constante - les chaînes de caractères sont entreposés à l'envers.
0300 Type 2: pointeur vers la définition de type
0400 Variable Pour les sous-variables d'un enregistrement, l'octet de poids faible du mot d'étiquette est le numéro de la définition d'enregistrement à laquelle appartient cette entrée.
2: pointeur vers la définition de type
4: décalage
5: 0 = normal, FF = indirect (VAR)
6: segment
FF = DS
FE = CS
FD = ES (via le pointeur)
.. = SS, le numéro correspond au niveau d'imbrication de la procédure
Pour les variables ABSOLUTE de format $B800:0, un pointeur est entreposé dans le segment de code, l'entrée de la table d'identificateurs de symboles CS indirecte.
0500 Procedure Procédure
0600 Function - 2: fonction uniquement - pointeur vers le type de résultat
- 4: fonction uniquement - déplacement sur la pile
- 5: fonction uniquement - 0 = normal, FF = indirect
- 6: fonction uniquement - segment: SS
- 8: décalage de code de procédure / fonction
- A: position dans le fichier de recouvrement / du saut vers l'avant
- C: 0 = ok, FF = définition avant
- E: durée de la procédure de superposition
-10: nombre de listes de paramètres :

- 2: type pointer | peut être répété
- 3: 0 = normal, FF = paramètre VAR |
- 4: nombre de paramètres de ce type |
- 5: noms |
Les paramètres sont également répertoriés en tant que variables locales.
0000, 0800 Subentries Sous-entrée

Structure des sous-entrées

Les entrées normales ne sont pas suffisantes pour la description de types complexes. Des sous-entrées sans nom sont utilisées pour cela. Pour les définitions complexes, cela donne une structure arborescente. ARRAY[1..15] OF INTEGER est un tableau ayant la sous-intervalle 1..15 comme type d'index et INTEGER comme type d'élément.

Recherche dans la table des symboles

Le Turbo Pascal 3 utilise une recherche linéaire pour trouver des symboles dans la table des symboles. Cela peut être lent. Un programme dans le pire des cas peut réduire la vitesse de compilation à 0,022 ligne par seconde...

Gestionnaire d'erreurs

De nombreux compilateurs essaient de continuer la compilation après qu'une erreur a été trouvée. La difficulté à ce sujet est que cela ne devrait pas déclencher une avalanche de messages d'erreur sans signification. Le Turbo Pascal 3 s'arrête toujours si une erreur est trouvée. Cela peut également être considéré comme un avantage : le programmeur est obligé de résoudre les problèmes un par un.

Bibliothèque d'exécution

Dans la bibliothèque d'exécution, toutes les procédures et fonctions standard nécessaires à l'exécution des programmes sont entreposées. Le Turbo Pascal 3 le fait d'une manière plutôt inutile (mais simple): il insère toujours toutes les procédures. Les versions ultérieures du compilateur incluent uniquement les procédures étant réellement utilisées.

Carte mémoire

Les segments sont répartis comme suit :

Tas

L'entreposage du tas est alloué en blocs dont la taille est un multiple de 8 octets. Les blocs libres sont conservés avec cette structure :

Position Description
+0 Ce champ contient un pointeur vers le bloc libre suivant → liste chaînée.
+4 Ce champ contient la longueur du bloc libre.
+8 Ce champ contient un bloc libre.

Le Hpstrt pointe vers le premier bloc libre. Le dernier bloc de la liste - l'espace libre entre la pile et le tas - est marqué par un 0. S'il n'y a pas assez d'espace entre le tas et la pile, une erreur est signalée.

Arithmétique à virgule flottante

Les nombres à réels (à virgule flottante) sont divisés en deux parties : l'exposant donne l'ordre de grandeur, la mantisse donne la précision nécessaire. Voici la formule mathématique associé :

nombre réel = mantisse x 2exposant

La mantisse est une fraction binaire avec une précision de 40 bits. La mantisse représente toujours un nombre compris entre 0,5 et 1, c'est-à-dire qu'elle est normalisée. Cette situation signifie que le bit le plus significatif est toujours 1. Ici, le signe est entreposé. Les exemples utilisent des nombres décimaux pour plus de simplicité.

Veuillez noter que les additions et soustractions en virgule flottante peuvent provoquer de grosses erreurs d'arrondi, si les exposants diffèrent trop. On prétend souvent que l'arithmétique BCD cause moins d'erreurs. Ce n'est pas correct - ils ne sont pas aussi visibles. Certaines calculatrices effectuent en fait des arrondis cosmétiques, si le résultat est proche d'un nombre entier. La meilleure façon d'éviter les erreurs d'arrondi est de calculer les montants en cents et non en dollars. Les multiplications et les divisions BCD sont lentes. Cependant, BCD a un avantage: les conversions vers et depuis ASCII sont beaucoup plus rapides. Comme les applications métier consistent généralement principalement en des ajouts et des soustractions, le BCD peut en fait être plus rapide.

Les racines carrées sont évaluées en utilisant l'approximation de Newton. C'est une bonne solution, mais pas la meilleure : le 8087 fait des racines carrées plus rapidement que les divisions ! Les fonctions transcendantales sont évaluées à l'aide des polynômes standard trouvés dans les livres de mathématiques. Ne vous attendez pas à beaucoup de vitesse et de précision des transcendantaux Turbo Pascal 3.

Bugs

Grâce à la relative simplicité des algorithmes utilisés, le Turbo Pascal 3 est quasiment exempt de bogues. Enfin presque.

Définir comme paramètre de procédure

Si le dernier élément est dans l'intervalle de 248..255 et le premier au-dessus de 7, cela ne fonctionne pas. Redéfinissez l'ensemble ou transmettez-le en tant que paramètre VAR.

SizeOf

Parfois, une charge redondante est émise.

UpCase

Le type de paramètre n'est pas vérifié. Essayez UpCase(15).

WHILE

Le mot réservé DO peut être omis.

Vitesse du compilateur

Le Turbo Pascal 3 est peut-être plus rapide que la plupart des autres compilateurs, mais il y a encore une large marge d'améliorations :

L'éditeur pourrait être beaucoup plus rapide. L'écran est ré-affiché de manière intelligente (en utilisant DelLine et InsLine). Sur un terminal, cela peut être plus rapide - avec la vidéo cartographiée en mémoire, c'est une nuisance. La recherche et le remplacement sont lents.

Écrire des programmes plus rapides avec Turbo Pascal 3

Évitez les fonctions de chaîne de caractères standard :

Écrivez des constantes réelles avec un point décimal (10.0 au lieu de 10). Ce détail élimine les conversions. Utiliser GOTO si nécessaire. Les GOTO de Turbo Pascal 3 sont meilleurs que les GOTO de BASIC : les noms peuvent être utilisés à la place des nombres. Les enmtrée/sorties peuvent être améliorées si des tampons plus grands sont utilisés (utilisez des multiples de 512 octets pour de meilleurs résultats). Utilisez BlockRead et BlockWrite. Si de gros blocs sont lus ou écrits, la surcharge du DOS et Turbo Pascal 3 n'a pas autant d'importance que pour les petits blocs.

Dernière mise à jour : Dimanche, le 20 avril 2014