Coulisses du turbo pascal : Différence entre versions

De Wiki expérimental
(Page créée avec « La Bible Turbo Pascal 2-36 Nous allons nous rendre maintenant derrière la scène d'un programme écrit en Turbo Pascal pour faire connaissance avec les acteurs et leurs a... »)
 
(Addr, opérateur @)
(6 révisions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
La Bible Turbo Pascal 2-36
 
 
Nous allons nous rendre maintenant derrière la scène d'un programme écrit en Turbo Pascal pour faire connaissance avec les acteurs et leurs accessoires. Nous intéresserons avant tout à la structure de base d'un tel programme, au déroulement des fonctions et procédures ainsi qu'à la mémorisation des variables. Vous découvrirez les particularités de l'unité System et vous pourrez jeter un coup d'oeil sur l'organisation du tas (heap).
 
Nous allons nous rendre maintenant derrière la scène d'un programme écrit en Turbo Pascal pour faire connaissance avec les acteurs et leurs accessoires. Nous intéresserons avant tout à la structure de base d'un tel programme, au déroulement des fonctions et procédures ainsi qu'à la mémorisation des variables. Vous découvrirez les particularités de l'unité System et vous pourrez jeter un coup d'oeil sur l'organisation du tas (heap).
 
Il ne s'agit pas d'être simplement curieux. La connaissance du mécanisme de fonctionnement d'un programme Turbo Pascal est indispensable pour comprendre de nombreuses manipulations présentées dans les chapitres qui suivent. Il n'est pas possible de réaliser des programmes résidents, de gérer des stockages de programmes temporaires ou de réaliser une unité multitâche sans ces notions.
 
Il ne s'agit pas d'être simplement curieux. La connaissance du mécanisme de fonctionnement d'un programme Turbo Pascal est indispensable pour comprendre de nombreuses manipulations présentées dans les chapitres qui suivent. Il n'est pas possible de réaliser des programmes résidents, de gérer des stockages de programmes temporaires ou de réaliser une unité multitâche sans ces notions.
 
Laissez vous guider à travers ce monde fascinant de bits et d'octets.
 
Laissez vous guider à travers ce monde fascinant de bits et d'octets.
2.1. Le modèle mémoire de Turbo Pascal
+
 
LePSP...........................................................39 Le bloc d'environnement ...........................................41
+
==Le modèle mémoire de Turbo Pascal==
 +
 
 
La première question qui se pose quand on commence à parcourir les coulisses d'un programme est de savoir où se trouvent ses différents éléments: code, données, pile et tas. En d'autres termes on cherche à définir le modèle mémoire que le compilateur utilise quand il transforme les instructions Pascal en langage machine.
 
La première question qui se pose quand on commence à parcourir les coulisses d'un programme est de savoir où se trouvent ses différents éléments: code, données, pile et tas. En d'autres termes on cherche à définir le modèle mémoire que le compilateur utilise quand il transforme les instructions Pascal en langage machine.
 +
 
Turbo Pascal fait tout pour simplifier cette recherche: car à l'inverse du langage C par exemple il ne connaît qu'un seul modèle mémoire auquel il se tient invariablement. Cette intransigeance limite certes la liberté du programmeur, mais elle lui évite aussi les tourments d'un choix délicat (les débutants qui se frottent à C en savent quelque chose).
 
Turbo Pascal fait tout pour simplifier cette recherche: car à l'inverse du langage C par exemple il ne connaît qu'un seul modèle mémoire auquel il se tient invariablement. Cette intransigeance limite certes la liberté du programmeur, mais elle lui évite aussi les tourments d'un choix délicat (les débutants qui se frottent à C en savent quelque chose).
 +
 
Le modèle mémoire de Turbo Pascal est assez différent de ceux qui sont habituellement pratiqués par les autres compilateurs ( Seul Quick Pascal imite ce modèle pour des raisons de compatibilité).
 
Le modèle mémoire de Turbo Pascal est assez différent de ceux qui sont habituellement pratiqués par les autres compilateurs ( Seul Quick Pascal imite ce modèle pour des raisons de compatibilité).
 
Si on cherche à tout prix une analogie, on pourra néanmoins le rapprocher du modèle MEDIUM défini par C. Un programme en Turbo Pascal contient de multiples segments de code, chacun d'eux pouvant épuiser la limite des 64 Ko inhérente au processeur 8086/8088. Le programme principal possède son propre segment, ainsi que chaque unité. Comme un programme peut contenir un nombre quelconque d'unités, la taille du code n'est limitée que par l'espace mémoire disponible et peut aller jusqu'à I Mo. Chaque programme possède au moins deux segments de-code : le segment du programme principal et celui de l'unité System que Turbo Pascal incorpore systématiquement sans même qu'on le demande.
 
Si on cherche à tout prix une analogie, on pourra néanmoins le rapprocher du modèle MEDIUM défini par C. Un programme en Turbo Pascal contient de multiples segments de code, chacun d'eux pouvant épuiser la limite des 64 Ko inhérente au processeur 8086/8088. Le programme principal possède son propre segment, ainsi que chaque unité. Comme un programme peut contenir un nombre quelconque d'unités, la taille du code n'est limitée que par l'espace mémoire disponible et peut aller jusqu'à I Mo. Chaque programme possède au moins deux segments de-code : le segment du programme principal et celui de l'unité System que Turbo Pascal incorpore systématiquement sans même qu'on le demande.
2-36 La Bible Turbo Pascal
 
2-37 Dans les coulisses du langage
 
FreePlr - Contenu du
 
PrefixSeg fichier EXE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Figure 13 Le modèle mémoire de Turbo Pascal
 
 
Quand un programme Turbo vient d'être chargé par le loader EXEC de DOS, les segments de code se situent tout à fait au début. Leur ordre n'est pas quelconque, mais obéit à des règles strictes. Le segment de code du programme principal précède les segments de code des unités qui apparaissent dans l'ordre inverse de leur énumération par USES.
 
Quand un programme Turbo vient d'être chargé par le loader EXEC de DOS, les segments de code se situent tout à fait au début. Leur ordre n'est pas quelconque, mais obéit à des règles strictes. Le segment de code du programme principal précède les segments de code des unités qui apparaissent dans l'ordre inverse de leur énumération par USES.
 +
 
Si le programme principal contient l'instruction:
 
Si le programme principal contient l'instruction:
Dans les coulisses du langage 2-37
+
 
I Uses Uniti, Unit2. Unit3;
+
Uses Uniti, Unit2. Unit3;
 +
 
 
le segment de code du programme principal est suivi par le segment de code de Unit3, puis par Unit2 et Uniti. Le dernier segment de code est toujours celui de l'unité System.
 
le segment de code du programme principal est suivi par le segment de code de Unit3, puis par Unit2 et Uniti. Le dernier segment de code est toujours celui de l'unité System.
 
Le premier segment de code est précédé par une structure appelée PSI' (Program Segment Prefix) ou préfixe du segment du programme. La présence du PSI' n'est pas due à Turbo Pascal mais au loader EXEC. Chaque fois que la loader charge un programme EXE, il le fait précéder en mémoire par le PSI'. Cette zone de données contient en fait d'importantes informations utilisées par le système d'exploitation DOS pour faire fonctionner le programme.
 
Le premier segment de code est précédé par une structure appelée PSI' (Program Segment Prefix) ou préfixe du segment du programme. La présence du PSI' n'est pas due à Turbo Pascal mais au loader EXEC. Chaque fois que la loader charge un programme EXE, il le fait précéder en mémoire par le PSI'. Cette zone de données contient en fait d'importantes informations utilisées par le système d'exploitation DOS pour faire fonctionner le programme.
 +
 
Alors que la taille maximale du code paraît pratiquement illimitée, l'espace mémoire dévolu aux données paraît en contrepartie tout à fait mesquin: le segment de données
 
Alors que la taille maximale du code paraît pratiquement illimitée, l'espace mémoire dévolu aux données paraît en contrepartie tout à fait mesquin: le segment de données
Blocs fragmentés du tas
 
HeapPir
 
Mémoire libre
 
4 HeapOrg Le tas grandit dans le sens des adresses croissantes
 
I
 
Tampon doverlay
 
Segment de données Variables globales
 
Dseg Constantes typées
 
Segment de code de la bibliothèque d'exécution
 
Segment de code de l'unité "A"
 
Segment de code des autres unitées
 
 
CSeg -
 
Segment de code de l'unité "E" Segment de code du programme
 
Prefixe du segment du programme (PSP)
 
La Bible Turbo Pascal 2-38
 
 
est en effet limité à 64 Ko. L'inquiétude s'accroît quand on apprend que ce segment doit contenir non seulement les variables globales, mais également toutes les constantes typées aussi bien celles du programme principal que celles des unités incluses. Mais cette inquiétude n'est pas justifiée les 64 Ko s'avèrent en général plus que suffisants, et si jamais la place se faisait trop juste à l'intérieur du segment de données, il serait toujours possible de recourir au tas (heap). Ce dernier peut en général offrir plusieurs centaines de Ko -selon la quantité de mémoire installée et la place que prend déjà le code du programme concerné. Par ailleurs le tas se prête parfaitement à l'implémentation de structures dynamiques du genre arbre ou listes chaînées. Au demeurant ces objets particulièrement sympathiques, rendus populaires par le célèbre ouvrage de Niklaus Wirth ("Algorithmes et structures de données"), ne devraient manquer dans aucun programme en Pascal digne de ce nom.
 
est en effet limité à 64 Ko. L'inquiétude s'accroît quand on apprend que ce segment doit contenir non seulement les variables globales, mais également toutes les constantes typées aussi bien celles du programme principal que celles des unités incluses. Mais cette inquiétude n'est pas justifiée les 64 Ko s'avèrent en général plus que suffisants, et si jamais la place se faisait trop juste à l'intérieur du segment de données, il serait toujours possible de recourir au tas (heap). Ce dernier peut en général offrir plusieurs centaines de Ko -selon la quantité de mémoire installée et la place que prend déjà le code du programme concerné. Par ailleurs le tas se prête parfaitement à l'implémentation de structures dynamiques du genre arbre ou listes chaînées. Au demeurant ces objets particulièrement sympathiques, rendus populaires par le célèbre ouvrage de Niklaus Wirth ("Algorithmes et structures de données"), ne devraient manquer dans aucun programme en Pascal digne de ce nom.
 +
 
Pour que les constantes typées soient toujours à la disposition immédiate de Turbo Pascal, le registre DS pointe constamment sur le segment de données correspondant tout au long de l'exécution du programme. L'adresse de ce segment, c'est-à-dire le contenu du registre DS, peut être connue à tout moment au moyen de la variable prédéfinie DSeg.
 
Pour que les constantes typées soient toujours à la disposition immédiate de Turbo Pascal, le registre DS pointe constamment sur le segment de données correspondant tout au long de l'exécution du programme. L'adresse de ce segment, c'est-à-dire le contenu du registre DS, peut être connue à tout moment au moyen de la variable prédéfinie DSeg.
 +
 
Juste après le segment de données se trouve la pile qui est destinée à stocker temporairement les adresses de retour des sous-programmes et les variables dites locales. Sa taille par défaut est de 16 Ko (16384 octets). A l'intérieur de l'EDI (environnement de développement intégré) elle peut être paramétrée par l'option "Compiler/Memory" du menu "Options". A l'intérieur des programmes elle peut être modifiée par la directive de compilation ($M). Pendant l'exécution d'un programme, le registre SS pointe constamment sur le segment de pile. Son adresse est contenue dans la variable prédéfinie SSeg.
 
Juste après le segment de données se trouve la pile qui est destinée à stocker temporairement les adresses de retour des sous-programmes et les variables dites locales. Sa taille par défaut est de 16 Ko (16384 octets). A l'intérieur de l'EDI (environnement de développement intégré) elle peut être paramétrée par l'option "Compiler/Memory" du menu "Options". A l'intérieur des programmes elle peut être modifiée par la directive de compilation ($M). Pendant l'exécution d'un programme, le registre SS pointe constamment sur le segment de pile. Son adresse est contenue dans la variable prédéfinie SSeg.
 +
 
Si SS reste constant, il n'en est pas de même du registre SP qui varie considérablement au cours d'une exécution. Au début du programme, SP pointe sur la mémoire qui se situe juste après le dernier octet du segment de la pile. A chaque appel de sous-programme, à chaque transmission de paramètres aux fonctions et procédures, le pointeur SP se déplace vers le début ou la fin du segment de pile. Sa position courante peut être obtenue en examinant le contenu de la variable SPtr. Tout comme SSeg, DSeg et CSeg, cette variable est déclarée automatiquement par l'unité System. Vous apprendrez dans la suite de cet ouvrage comment manier la pile en fonction des variables locales stockées et des paramètres transmis aux fonctions et aux procédures.
 
Si SS reste constant, il n'en est pas de même du registre SP qui varie considérablement au cours d'une exécution. Au début du programme, SP pointe sur la mémoire qui se situe juste après le dernier octet du segment de la pile. A chaque appel de sous-programme, à chaque transmission de paramètres aux fonctions et procédures, le pointeur SP se déplace vers le début ou la fin du segment de pile. Sa position courante peut être obtenue en examinant le contenu de la variable SPtr. Tout comme SSeg, DSeg et CSeg, cette variable est déclarée automatiquement par l'unité System. Vous apprendrez dans la suite de cet ouvrage comment manier la pile en fonction des variables locales stockées et des paramètres transmis aux fonctions et aux procédures.
 +
 
A la fin du segment de pile commence la zone du tas (heap). Le tas permet l'allocation dynamique de mémoire au moyen des procédures New et GetMem. Si sa taille n'est pas limitée par la directive de compilation ($M) ou par l'option "Compiler/Memory" du menu "Options", le tas utilise toute la mémoire disponible jusqu'à la frontière des 640 Ko.
 
A la fin du segment de pile commence la zone du tas (heap). Le tas permet l'allocation dynamique de mémoire au moyen des procédures New et GetMem. Si sa taille n'est pas limitée par la directive de compilation ($M) ou par l'option "Compiler/Memory" du menu "Options", le tas utilise toute la mémoire disponible jusqu'à la frontière des 640 Ko.
 +
 
Entre le segment de pile et le tas, Turbo Pascal intercale le tampon des recouvrements (overlay buffer) lorsqu'un programme travaille avec des overlays (ou recouvrements) et qu'il inclut à cet effet l'unité Overlay dans une instruction USES. Le tampon se réserve la place suffisante pour contenir le plus grand des fichiers de recouvrement, et par conséquent tous les autres qui viendraient s'y substituer et qui sont nécessairement plus petits.
 
Entre le segment de pile et le tas, Turbo Pascal intercale le tampon des recouvrements (overlay buffer) lorsqu'un programme travaille avec des overlays (ou recouvrements) et qu'il inclut à cet effet l'unité Overlay dans une instruction USES. Le tampon se réserve la place suffisante pour contenir le plus grand des fichiers de recouvrement, et par conséquent tous les autres qui viendraient s'y substituer et qui sont nécessairement plus petits.
2-38 La Bible Turbo Pascal
+
 
2-39 Dans les coulisses du langage
+
===Le PSP===
V Le PSP
+
 
 
Le PSP est une structure de données mise en place par le loader EXEC qui à l'intérieur du noyau de DOS est responsable du chargement des programmes COM et E)Œ. Comme son nom l'indique, le PSI' est disposé en tête du programme dans la mémoire centrale. Il occupe 256 octets (soit 16 paragraphes) et se situe, dans le cas d'un programme compilé en Turbo Pascal, juste avant le segment de code. Si l'adresse du segment de code est par exemple $3Fb, le PSP commence au segment d'adresse $3F00. Mais à vrai dire vous n'êtes pas censé vous livrer à ce genre de calcul car Turbo Pascal dispose l'adresse du segment du PSP dans une variable prédéfinie appelée PrefixSeg.
 
Le PSP est une structure de données mise en place par le loader EXEC qui à l'intérieur du noyau de DOS est responsable du chargement des programmes COM et E)Œ. Comme son nom l'indique, le PSI' est disposé en tête du programme dans la mémoire centrale. Il occupe 256 octets (soit 16 paragraphes) et se situe, dans le cas d'un programme compilé en Turbo Pascal, juste avant le segment de code. Si l'adresse du segment de code est par exemple $3Fb, le PSP commence au segment d'adresse $3F00. Mais à vrai dire vous n'êtes pas censé vous livrer à ce genre de calcul car Turbo Pascal dispose l'adresse du segment du PSP dans une variable prédéfinie appelée PrefixSeg.
 +
 
Adresse tontenu Type
 
Adresse tontenu Type
 
$00 Appel de l'interruption 20h 2 BYTE
 
$00 Appel de l'interruption 20h 2 BYTE
Ligne 76 : Ligne 53 :
 
(excepté le Retour Chariot terminal)
 
(excepté le Retour Chariot terminal)
 
$81 Ligne de commande 127 BYTE
 
$81 Ligne de commande 127 BYTE
 +
 
Longueur totale: 256 octets
 
Longueur totale: 256 octets
 +
 
Figure 14 Structure du PSP (Préfixe du Segment du Programme)
 
Figure 14 Structure du PSP (Préfixe du Segment du Programme)
 +
 
Le système DOS utilise le PSI' pour gérer le programme. Il s'y trouve quelques informations qui servent dans certains cas à "tricher" un peu, c'est-à-dire à sortir des règles imposées par la syntaxe de Turbo Pascal. Tous les champs n'ont pas la même utilité car la structure du PSP n'a pas évolué depuis la version 1.0 de DOS alors que les développeurs de Microsoft ont largement modifié-le noyau du système.
 
Le système DOS utilise le PSI' pour gérer le programme. Il s'y trouve quelques informations qui servent dans certains cas à "tricher" un peu, c'est-à-dire à sortir des règles imposées par la syntaxe de Turbo Pascal. Tous les champs n'ont pas la même utilité car la structure du PSP n'a pas évolué depuis la version 1.0 de DOS alors que les développeurs de Microsoft ont largement modifié-le noyau du système.
 +
 
C'est ainsi qu'on trouve en tête du PSI' un appel à l'interruption 20h qui servait autrefois à clôturer un programme mais qui n'est plus utilisée aujourd'hui car elle est remplacée par une autre fonction.
 
C'est ainsi qu'on trouve en tête du PSI' un appel à l'interruption 20h qui servait autrefois à clôturer un programme mais qui n'est plus utilisée aujourd'hui car elle est remplacée par une autre fonction.
 +
 
Le deuxième champ est beaucoup plus intéressant: il indique à DOS l'adresse du segment de la dernière mémoire allouée au programme.
 
Le deuxième champ est beaucoup plus intéressant: il indique à DOS l'adresse du segment de la dernière mémoire allouée au programme.
Dans les coulisses du langage 2-39
+
 
La Bible Turbo Pascal 2-40
+
 
Le troisième champ porte le titre "réservé" ce qui veut dire en fait "non documenté". De nombreux programmeurs se sont attachés à décrypter la signification de ce type de champ, et certains d'entre eux y sont même parvenus. Mais cette signification peut être modifiée sans préavis d'une version de DOS à l'autre. De toute façon, nous n'en parlerons pas car la programmation en Turbo Pascal n'est pas concernée.
 
Le troisième champ porte le titre "réservé" ce qui veut dire en fait "non documenté". De nombreux programmeurs se sont attachés à décrypter la signification de ce type de champ, et certains d'entre eux y sont même parvenus. Mais cette signification peut être modifiée sans préavis d'une version de DOS à l'autre. De toute façon, nous n'en parlerons pas car la programmation en Turbo Pascal n'est pas concernée.
 +
 
A l'adresse 5 se trouve un appel au répartiteur de fonction qui se cache derrière l'interruption 21h et qui coordonne l'aiguillage des fonctions de DOS gérées par cette interruption. Cette indication est également obsolète et dénuée de tout intérêt.
 
A l'adresse 5 se trouve un appel au répartiteur de fonction qui se cache derrière l'interruption 21h et qui coordonne l'aiguillage des fonctions de DOS gérées par cette interruption. Cette indication est également obsolète et dénuée de tout intérêt.
 +
 
Par contre les trois champs suivants ont plus d'importance : le système DOS y mémorise les contenus de trois vecteurs d'interruption qui sont souvent "détournés" par les programmes et qui doivent donc être rétablis à leur valeur initiale à la fin de l'exécution. L'interruption 22h sert à clôturer un programme et est invoquée par toutes les fonctions DOS que le programmeur met en service pour terminer un programme. L'interruption 23h est une routine déclenchée par la combinaison des touches "Contrôle C" et qui sert à interrompre brutalement mais en toute régularité le programme en cours d'exécution. L'interruption 24h pointe sur une routine que DOS appelle en cas d'erreur critique. C'est elle qui émet par exemple le célèbre message "Abandon, Reprise, Ignore?".
 
Par contre les trois champs suivants ont plus d'importance : le système DOS y mémorise les contenus de trois vecteurs d'interruption qui sont souvent "détournés" par les programmes et qui doivent donc être rétablis à leur valeur initiale à la fin de l'exécution. L'interruption 22h sert à clôturer un programme et est invoquée par toutes les fonctions DOS que le programmeur met en service pour terminer un programme. L'interruption 23h est une routine déclenchée par la combinaison des touches "Contrôle C" et qui sert à interrompre brutalement mais en toute régularité le programme en cours d'exécution. L'interruption 24h pointe sur une routine que DOS appelle en cas d'erreur critique. C'est elle qui émet par exemple le célèbre message "Abandon, Reprise, Ignore?".
 +
 
Comme nous le verrons, Turbo Pascal détourne les deux dernières interruptions citées pour substituer aux routines habituelles de DOS ses propres routines internes issues de l'unité System. Il est indispensable d'employer de tels moyens pour réaliser par exemple un contrôle indépendant des entrées-sorties et empêcher qu'un programme soit interrompu par "Ctrl C".
 
Comme nous le verrons, Turbo Pascal détourne les deux dernières interruptions citées pour substituer aux routines habituelles de DOS ses propres routines internes issues de l'unité System. Il est indispensable d'employer de tels moyens pour réaliser par exemple un contrôle indépendant des entrées-sorties et empêcher qu'un programme soit interrompu par "Ctrl C".
 +
 
Les trois vecteurs d'interruption sont suivis par un champ réservé, après quoi on trouve l'adresse du segment du bloc d'environnement. Des explications détaillées sur ce bloc vont vous être données au paragraphe suivant.
 
Les trois vecteurs d'interruption sont suivis par un champ réservé, après quoi on trouve l'adresse du segment du bloc d'environnement. Des explications détaillées sur ce bloc vont vous être données au paragraphe suivant.
 +
 
Après l'adresse du bloc d'environnement on trouve encore un certain nombre de champs non documentés. Puis le PSP prévoit de la place pour deux FCB installés automatiquement par DOS. "FCB" est un acronyme pour "File Control Block, une structure de données que DOS utilisait jusqu'à la version 2.0 pour accéder aux fichiers. Depuis la version 2.0 les fonctions du FCB ont été remplacées par les fonctions de "handle" qui sont plus faciles à utiliser pour accéder aux fichiers et aux périphériques. De nos jours les FCB ne jouent plus aucun rôle dans la programmation des PC.
 
Après l'adresse du bloc d'environnement on trouve encore un certain nombre de champs non documentés. Puis le PSP prévoit de la place pour deux FCB installés automatiquement par DOS. "FCB" est un acronyme pour "File Control Block, une structure de données que DOS utilisait jusqu'à la version 2.0 pour accéder aux fichiers. Depuis la version 2.0 les fonctions du FCB ont été remplacées par les fonctions de "handle" qui sont plus faciles à utiliser pour accéder aux fichiers et aux périphériques. De nos jours les FCB ne jouent plus aucun rôle dans la programmation des PC.
Les deux derniers champs du PSP contiennent par contre des informations extrêmement importantes: il s'agit en effet des arguments de la ligne de commande. Le loader EXEC mémorise à partir de l'offset $81 tous les caractères qui suivent le nom du programme appelé. Sont exclus les indications servant au détournement des entrées-sorties (> et <) et les arguments liés à l'enchaînement (I) de plusieurs instructions. La longueur de la chaîne jusqu'au Retour Chariot (Code ASCII 13), autrement dit le nombre des caractères qui constituent les arguments, est stockée à l'adresse de l'offset $80. Pour être précis, signalons que le Retour Chariot n'est pas pris en compte dans l'estimation de la longueur de la chaîne car il ne provient pas de la ligne'de commande mais a été rajouté par le loader EXEC.
+
Les deux derniers champs du PSP contiennent par contre des informations extrêmement importantes: il s'agit en effet des arguments de la ligne de commande. Le loader EXEC mémorise à partir de l'offset $81 tous les caractères qui suivent le nom du programme appelé. Sont exclus les indications servant au détournement des entrées-sorties (> et <) et les arguments liés à l'enchaînement (I) de plusieurs instructions. La longueur de la chaîne jusqu'au Retour Chariot (Code ASCII 13), autrement dit le nombre des caractères qui constituent les arguments, est stockée à l'adresse de l'offset $80. Pour être précis, signalons que le Retour Chariot n'est pas pris en compte dans l'estimation de la longueur de la chaîne car il ne provient pas de la ligne'de commande mais a été rajouté par le loader  
2-40 La Bible Turbo Pascal
+
EXEC.
2-41 Dans les coulisses du langage
+
 
 
Comme il n'existe pas d'autre possibilité de connaître ces informations, Turbo Pascal lit les champs correspondants au début du programme principal. Le nombre d'arguments décelés est transmis à la variable ParamCount. Les arguments eux-mêmes sont accessibles au moyen de la fonction ParamStr.
 
Comme il n'existe pas d'autre possibilité de connaître ces informations, Turbo Pascal lit les champs correspondants au début du programme principal. Le nombre d'arguments décelés est transmis à la variable ParamCount. Les arguments eux-mêmes sont accessibles au moyen de la fonction ParamStr.
V Le bloc d'environnement
+
 
 +
===Le bloc d'environnement===
 +
 
 
Le loader EXEC écrit dans le PSP une copie du bloc d'environnement gérée par l'interpréteur de commandes COMMAND.COM. Il s'agit d'une collection de chaînes de caractères dans laquelle l'interpréteur de commandes et d'autres programmes trouvent diverses informations, comme par exemple le chemin d'accès aux fichiers exécutables (PATH). Chacune des chaînes qui constituent le bloc a normalement la forme:
 
Le loader EXEC écrit dans le PSP une copie du bloc d'environnement gérée par l'interpréteur de commandes COMMAND.COM. Il s'agit d'une collection de chaînes de caractères dans laquelle l'interpréteur de commandes et d'autres programmes trouvent diverses informations, comme par exemple le chemin d'accès aux fichiers exécutables (PATH). Chacune des chaînes qui constituent le bloc a normalement la forme:
Nom-Paramètre I
+
 
 +
Nom-Paramètre I
 +
 
 
et se termine par un caractère NUL (de code ASCII 0). Les chaînes sont disposées bout à bout, de sorte que le caractère terminal de l'une est suivi du premier caractère de la suivante. La fin du bloc d'environnement, dont la longueur est limitée à 32 Ko, est signalée par un caractère de clôture qui suit le caractère terminal de la dernière chaîne.
 
et se termine par un caractère NUL (de code ASCII 0). Les chaînes sont disposées bout à bout, de sorte que le caractère terminal de l'une est suivi du premier caractère de la suivante. La fin du bloc d'environnement, dont la longueur est limitée à 32 Ko, est signalée par un caractère de clôture qui suit le caractère terminal de la dernière chaîne.
 +
 
A partir de la version 3.0 de DOS, la dernière chaîne du bloc d'environnement peut être suivie d'autres chaînes. Le nombre de ces chaînes supplémentaires est alors indiqué sous la forme d'un mot placé juste après la dernière chaîne du bloc d'environnement. A l'heure actuelle, DOS ne gère qu'une seule chaîne supplémentaire, le mot en question contient donc la valeur 1. La chaîne supplémentaire contient la désignation de l'unité et le chemin d'accès complet du programme en cours d'exécution, c'est-à-dire de celui auquel appartient le PSP. Grâce à cette information contenue dans le bloc d'environnement étendu, un programme peut par exemple détecter le répertoire d'où il a été lancé et rechercher automatiquement les overlays, fichiers de configuration et autres fichiers annexes dans le même répertoire sans avoir à interroger l'utilisateur à ce sujet.
 
A partir de la version 3.0 de DOS, la dernière chaîne du bloc d'environnement peut être suivie d'autres chaînes. Le nombre de ces chaînes supplémentaires est alors indiqué sous la forme d'un mot placé juste après la dernière chaîne du bloc d'environnement. A l'heure actuelle, DOS ne gère qu'une seule chaîne supplémentaire, le mot en question contient donc la valeur 1. La chaîne supplémentaire contient la désignation de l'unité et le chemin d'accès complet du programme en cours d'exécution, c'est-à-dire de celui auquel appartient le PSP. Grâce à cette information contenue dans le bloc d'environnement étendu, un programme peut par exemple détecter le répertoire d'où il a été lancé et rechercher automatiquement les overlays, fichiers de configuration et autres fichiers annexes dans le même répertoire sans avoir à interroger l'utilisateur à ce sujet.
230:0088 434F ID 53584513 3D-43 3A SC 434F 4F 4D 41 COMSFEC:C:\COMP1( 2380818 4E 442E 434F 4F 0058-415448 3D 43 3A 5C 44 ND.CON.PATH:C\D 20:8028 4F 53 31143 3A SC 42 41-54 43400858524E 4F OS;C:\BATCH,P11OM 2A38838 5854 3D 46 72 65 64 2C-28 69 6C 20 65 73 74 28 PT:FretL il est 200048 2474246824682468-2865742874752865 $t$hh$h et tu e 2A38:8058 7328737572202470-245E 08080188 5C 43 s sur p$., .\C 20:0068 4F 4F 4D 414E 442E 43-4F ID 001120389 2A 83 OPUIAND.COH,.,.*. 20:0878 SA 38 2A CO 754226FF-36 lA 088E 8654 4226 Z8*.uB&.6J ... TB&
+
 
Figure 15 Exemple de bloc d'environnement
+
(image de la mémoire centrale obtenue par DEBUG)
+
 
Au niveau de l'utilisateur, ce sont les commandes SET et PATH de DOS qui servent à manipuler le bloc d'environnement. Ces commandes permettent d'y ajouter ou d'en modifier une chaîne.
 
Au niveau de l'utilisateur, ce sont les commandes SET et PATH de DOS qui servent à manipuler le bloc d'environnement. Ces commandes permettent d'y ajouter ou d'en modifier une chaîne.
Dans les coulisses du langage 2-41
+
 
La Bible Turbo Pascal 2-42
+
 
En exploitant l'adresse du segment du bloc d'environnement située au déplacement $2C du PSP, Turbo Pascal se ménage un accès aux informations déposées à cet endroit. L'unité DOS dispose à cet effet des fonctions EnrStr et GetEnv, tandis que le nombre des paramètres est tenu à jour dans la variable EnvCount.
 
En exploitant l'adresse du segment du bloc d'environnement située au déplacement $2C du PSP, Turbo Pascal se ménage un accès aux informations déposées à cet endroit. L'unité DOS dispose à cet effet des fonctions EnrStr et GetEnv, tandis que le nombre des paramètres est tenu à jour dans la variable EnvCount.
 +
 
Il est intéressant de noter que la dernière chaîne qui donne le nom et le chemin d'accès du programme exécuté n'est pas prise en compte à te niveau. Mais elle est retournée au niveau de l'unité System par la fonction ParamStr munie de l'argument O.
 
Il est intéressant de noter que la dernière chaîne qui donne le nom et le chemin d'accès du programme exécuté n'est pas prise en compte à te niveau. Mais elle est retournée au niveau de l'unité System par la fonction ParamStr munie de l'argument O.
2.2. Constantes typées et constantes non typées
+
 
 +
==Constantes typées et constantes non typées==
 +
 
 
En Turbo Pascal les constantes typées sont stockées avec toutes les autres variables
 
En Turbo Pascal les constantes typées sont stockées avec toutes les autres variables
lobales dans le segment commun des données. Cette disposition n'est guère etonnante, car les constantes typées ne se distinguent des variables ordinaires que par le fait qu'elles sont initialisées par le programmeur, alors que les autres variables ont une valeur indéterminée au début de l'exécution du programme. Les constantes typées déclarées localement dans une procédure ou une fonction sont également stockées dans le segment des données, quoique dans ce cas elles ne soient accessibles que de l'intérieur de la procédure ou de la fonction en question.
+
locales dans le segment commun des données. Cette disposition n'est guère etonnante, car les constantes typées ne se distinguent des variables ordinaires que par le fait qu'elles sont initialisées par le programmeur, alors que les autres variables ont une valeur indéterminée au début de l'exécution du programme. Les constantes typées déclarées localement dans une procédure ou une fonction sont également stockées dans le segment des données, quoique dans ce cas elles ne soient accessibles que de l'intérieur de la procédure ou de la fonction en question.
 +
 
 
Malgré tout, les constantes typées ne sont pas mélangées aux variables globales à l'intérieur du segment des données. Elles se retrouvent ensemble au début du segment, sous la forme d'un grand bloc commun. Turbo Pascal les range à cet endroit pour économiser de la place mémoire à l'intérieur du fichier exécutable compilé. Le fichier E)(E, en effet, ne doit contenir que les éléments strictement indispensables au lancement du programme. Parmi ces éléments indispensables figure évidemment le code machine du programme, mais aussi les constantes typées qui doivent avoir une valeur initiale.
 
Malgré tout, les constantes typées ne sont pas mélangées aux variables globales à l'intérieur du segment des données. Elles se retrouvent ensemble au début du segment, sous la forme d'un grand bloc commun. Turbo Pascal les range à cet endroit pour économiser de la place mémoire à l'intérieur du fichier exécutable compilé. Le fichier E)(E, en effet, ne doit contenir que les éléments strictement indispensables au lancement du programme. Parmi ces éléments indispensables figure évidemment le code machine du programme, mais aussi les constantes typées qui doivent avoir une valeur initiale.
 +
 
Le contenu des variables globales est par définition indéterminé au moment du lancement du programme. C'est ainsi que ce dernier contient des références à ces variables, mais les variables elles-mêmes ne sont pas stockées: une place est prévue pour elles juste derrière les constantes typées.
 
Le contenu des variables globales est par définition indéterminé au moment du lancement du programme. C'est ainsi que ce dernier contient des références à ces variables, mais les variables elles-mêmes ne sont pas stockées: une place est prévue pour elles juste derrière les constantes typées.
Pour ce qui est des constantes non typées déclarées par l'instruction CONST, elles occupent parfois de la mémoire mais pas toujours. Cela dépend de leur type. En principe Turbo Pascal n'est obligé de réserver de la mémoire que pour les constantes référencées par des pointeurs. Pour les nombres, c'est-à-dire les types Integer et Real, cette remarque n'est pas valable car Turbo Pascal peut les transférer par des instructions MOV dans les registres du processeur ou les variables. Mais il en est autrement des chaînes de caractères. Celles-ci doivent être effectivement déposées dans la mémoire car Turbo Pascal les adresse uniquement par des pointeurs. Et il va de soi qu'un pointeur ne peut référencer qu'un objet qui se trouve réellement et physiquement en mémoire. Les constantes chaînes de caractères sont d'ailleurs plus nombreuses que vous ne le pensez a priori. Elles ne sont en effet pas seulement générées par des instructions du type:
+
 
const Message - Salut Turbo; I
+
Pour ce qui est des constantes non typées déclarées par l'instruction CONST, elles occupent parfois  
2-42 La Bible Turbo Pascal
+
de la mémoire mais pas toujours. Cela dépend de leur type. En principe Turbo Pascal n'est obligé de réserver de la mémoire que pour les constantes référencées par des pointeurs. Pour les nombres, c'est-à-dire les types Integer et Real, cette remarque n'est pas valable car Turbo Pascal peut les transférer par des instructions MOV dans les registres du processeur ou les variables. Mais il en est autrement des chaînes de caractères. Celles-ci doivent être effectivement déposées dans la mémoire car Turbo Pascal les adresse uniquement par des pointeurs. Et il va de soi qu'un pointeur ne peut référencer qu'un objet qui se trouve réellement et physiquement en mémoire. Les constantes chaînes de caractères sont d'ailleurs plus nombreuses que vous ne le pensez a priori. Elles ne sont en effet pas seulement générées par des instructions du type:
2-43 Dans les coulisses du langage
+
 +
const Message - Salut Turbo; I
 +
 
 
Des instructions comme
 
Des instructions comme
Iwriteln('Constante chaîne'); 1
+
 
 +
Iwriteln('Constante chaîne'); 1
 +
 
 
ou
 
ou
chaine–'Texte' I
+
 
 +
chaine–'Texte' I
 +
 
 
donnent automatiquement naissance à des constantes chaînes sans nom, les chaînes correspondantes devant être adressées comme les autres.
 
donnent automatiquement naissance à des constantes chaînes sans nom, les chaînes correspondantes devant être adressées comme les autres.
 +
 
Mais contrairement à ce qu'il fait pour les constantes typées, Turbo Pascal ne range pas ces constantes chaînes dans le segment des données. Il les place dans le segment du code, juste avant les procédures ou fonctions qui y font appel.
 
Mais contrairement à ce qu'il fait pour les constantes typées, Turbo Pascal ne range pas ces constantes chaînes dans le segment des données. Il les place dans le segment du code, juste avant les procédures ou fonctions qui y font appel.
 +
 
Les constantes chaînes qui apparaissent à plusieurs reprises dans une procédure ou une fonction ne sont stockées qu'une seule fois à un même endroit. Le listing qui suit assorti de l'extrait de mémoire correspondant montre le fonctionnement de Turbo Pascal dans ce domaine.
 
Les constantes chaînes qui apparaissent à plusieurs reprises dans une procédure ou une fonction ne sont stockées qu'une seule fois à un même endroit. Le listing qui suit assorti de l'extrait de mémoire correspondant montre le fonctionnement de Turbo Pascal dans ce domaine.
Prograrn Constantes;.J
+
 
const Salutation–'Salut' ;.J
+
Prograrn Constantes;.J
Enti erLong–$11112222 ;.J
+
const Salutation–'Salut' ;.J
EntierNormal–S4711 ;.i
+
Enti erLong–$11112222 ;.J
NombreReel-1 .2345678;_J
+
EntierNormal–S4711 ;.i
Procedure Test;.J
+
NombreReel-1 .2345678;_J
var r real;.J
+
Procedure Test;.J
1 : longlrit;.J
+
  var r real;.J
I : integer;.J
+
  1 : longlrit;.J
 +
  I : integer;.J
 +
 
 
begin.J
 
begin.J
 
writeln( Salutation );.J
 
writeln( Salutation );.J
Ligne 216 : Ligne 219 :
 
Figure 16: Début du segment de code du programme précédent. Le double stockage des
 
Figure 16: Début du segment de code du programme précédent. Le double stockage des
 
constantes chaîne 'Salut' et 'Turbo est aisément reconnaissable.
 
constantes chaîne 'Salut' et 'Turbo est aisément reconnaissable.
2-44 La Bible Turbo Pascal
+
 
2-45 Dans les coulisses du langage
+
 
Le listing montre effectivement que Turbo Pascal ne stocke pas en mémoire les constantes numériques mais qu'il les charge à chaque fois dans la variable concernée au moyen de l'instruction MOV. A l'inverse, les chaînes de caractères sont quant à elles systématiquement stockées en mémoire. Lorsque des chaînes identiques sont appelées par des procédures différentes, leur stockage est répété. Mais cette redondance est évitée lorsque la même chaîne réapparaît plusieurs fois au sein de la même procédure ou fonction.
 
Le listing montre effectivement que Turbo Pascal ne stocke pas en mémoire les constantes numériques mais qu'il les charge à chaque fois dans la variable concernée au moyen de l'instruction MOV. A l'inverse, les chaînes de caractères sont quant à elles systématiquement stockées en mémoire. Lorsque des chaînes identiques sont appelées par des procédures différentes, leur stockage est répété. Mais cette redondance est évitée lorsque la même chaîne réapparaît plusieurs fois au sein de la même procédure ou fonction.
 +
 
Cette façon de procéder peut paraître au premier abord illogique et inefficace, mais elle est en fait inévitable compte tenu du fonctionnement de l'éditeur de liens interne lorsqu'il relie le programme aux unités incluses. Le Turbo Linker est en effet suffisamment intelligent pour n'incorporer que le strict nécessaire, c'est-à-dire qu'il ne prend pour constituer le fichier exécutable que les procédures, fonctions et variables réellement appelées ou utilisées à l'intérieur du programme. Mais ces décisions sont encore prématurées au moment de la compilation, surtout pour ce qui concerne les unités. Une procédure P2 ne peut pas accéder aux constantes chaînes de la procédure PI cari! est possible que PI ne soit jamais invoqué et ne soit pas incorporé dans le fichier EXE. En cherchant la constante, la procédure P2 risquerait de ne rien trouver, ce qui conduirait au moment de l'édition des liens à une erreur "unresolved external".
 
Cette façon de procéder peut paraître au premier abord illogique et inefficace, mais elle est en fait inévitable compte tenu du fonctionnement de l'éditeur de liens interne lorsqu'il relie le programme aux unités incluses. Le Turbo Linker est en effet suffisamment intelligent pour n'incorporer que le strict nécessaire, c'est-à-dire qu'il ne prend pour constituer le fichier exécutable que les procédures, fonctions et variables réellement appelées ou utilisées à l'intérieur du programme. Mais ces décisions sont encore prématurées au moment de la compilation, surtout pour ce qui concerne les unités. Une procédure P2 ne peut pas accéder aux constantes chaînes de la procédure PI cari! est possible que PI ne soit jamais invoqué et ne soit pas incorporé dans le fichier EXE. En cherchant la constante, la procédure P2 risquerait de ne rien trouver, ce qui conduirait au moment de l'édition des liens à une erreur "unresolved external".
2.3. Variables
+
 
 +
==Variables==
 +
 
 
Types d'entiers ....................................................46 Types énumérés ...................................................46 Booléens..........................................................46 Caractères Char ...................................................47 Chaînes de caractères String ........................................47 Tableaux..........................................................47 Enregistrements Record ............................................48 Ensembles........................................................48 Pointeurs.........................................................48 Types réels (virgule flottante) .......................................49 Alignement des variables ..........................................49
 
Types d'entiers ....................................................46 Types énumérés ...................................................46 Booléens..........................................................46 Caractères Char ...................................................47 Chaînes de caractères String ........................................47 Tableaux..........................................................47 Enregistrements Record ............................................48 Ensembles........................................................48 Pointeurs.........................................................48 Types réels (virgule flottante) .......................................49 Alignement des variables ..........................................49
 +
 
Turbo Pascal stocke les variables globales dans le segment de données et les variables locales sur la pile (voir la section consacrée aux procédures et fonctions).
 
Turbo Pascal stocke les variables globales dans le segment de données et les variables locales sur la pile (voir la section consacrée aux procédures et fonctions).
 +
 
Cet énoncé lapidaire est insuffisant pour traiter le sujet car il nous faut répondre à d'autres questions: de quelle manière Turbo Pascal procède-t-il au stockage, quel est le format utilisé, comment les variables sont-elles traitées? Le présent chapitre se propose justement de répondre à ces questions.
 
Cet énoncé lapidaire est insuffisant pour traiter le sujet car il nous faut répondre à d'autres questions: de quelle manière Turbo Pascal procède-t-il au stockage, quel est le format utilisé, comment les variables sont-elles traitées? Le présent chapitre se propose justement de répondre à ces questions.
 +
 
Bien entendu Turbo Pascal n'est pas libre de choisir pour chaque type de donnée le format qui lui plaît. Mais il va s'efforcer de générer des programmes aussi rapides que possible en tenant compte des propriétés du processeur. Ce dernier ne sait pas ce qu'est une chaîne, une variable reelle ou booléenne, mais son jeu d'instructions détermine évidemment la manière dont vont être traitées les informations.
 
Bien entendu Turbo Pascal n'est pas libre de choisir pour chaque type de donnée le format qui lui plaît. Mais il va s'efforcer de générer des programmes aussi rapides que possible en tenant compte des propriétés du processeur. Ce dernier ne sait pas ce qu'est une chaîne, une variable reelle ou booléenne, mais son jeu d'instructions détermine évidemment la manière dont vont être traitées les informations.
Dans les coulisses du langage 2-45
+
 
La Bible Turbo Pascal 2-46
+
 
Turbo Pascal répartit les différents types en plusieurs groupes auxquels il associe divers formats de données.
 
Turbo Pascal répartit les différents types en plusieurs groupes auxquels il associe divers formats de données.
V Types d'entiers
+
 
Parmi les types d'entiers, Turbo Pascal distingue des mémorisations sur un, deux ou quatre octets. Au niveau du processeur les types correspondants sont BYTE, WORD et DWORD.
+
==Types d'entiers==
 +
 
 +
Parmi les types d'entiers, Turbo Pascal distingue des mémorisations sur un, deux ou quatre octets. Au  
 +
niveau du processeur les types correspondants sont BYTE, WORD et DWORD.
 +
 
 
Note: Pour distinguer les formats du processeur des formats de Turbo Pascal,
 
Note: Pour distinguer les formats du processeur des formats de Turbo Pascal,
 
nous écrirons toujours les premiers en lettres capitales.
 
nous écrirons toujours les premiers en lettres capitales.
 +
 
Pour stocker des octets, Turbo Pascal propose les types Byte et Shortlnt. La différence entre ces deux types tient à l'interprétation de leurs valeurs. Byte est un type de données non signé et sert donc à représenter un nombre compris entre O et 255. A l'inverse, le bit 7 d'un type Shortlnt est interprété comme un signe, ce qui étend le domaine de définition sur l'intervalle -128 à 127.
 
Pour stocker des octets, Turbo Pascal propose les types Byte et Shortlnt. La différence entre ces deux types tient à l'interprétation de leurs valeurs. Byte est un type de données non signé et sert donc à représenter un nombre compris entre O et 255. A l'inverse, le bit 7 d'un type Shortlnt est interprété comme un signe, ce qui étend le domaine de définition sur l'intervalle -128 à 127.
 +
 
La même distinction entre types signé et non signé s'applique aux entiers stockés sur deux octets et gérés au niveau machine par le format WORD. Turbo Pascal propose ainsi les deux types de données Word et Integer. Le type Integer est signé et son intervalle de définition va de -32768 à 32767. Les valeurs prises par un entier de type Word ne sont jamais négatives et vont de O à 65535.
 
La même distinction entre types signé et non signé s'applique aux entiers stockés sur deux octets et gérés au niveau machine par le format WORD. Turbo Pascal propose ainsi les deux types de données Word et Integer. Le type Integer est signé et son intervalle de définition va de -32768 à 32767. Les valeurs prises par un entier de type Word ne sont jamais négatives et vont de O à 65535.
 +
 
Si l'intervalle de définition n'est pas encore suffisamment étendu, il faut faire appel au type Longlnt. Au niveau interne il correspond à un DWORD sur 4 octets interprété sous forme signée. L'intervalle de définition va de -2147483648 à 2147483647. Pour mémoriser les entiers Longlnt, Turbo Pascal suit les conventions posées par INTEL pour les mots doubles DWORD. Le mot de poids inférieur est mis dans la mémoire d'adresse basse, le mot de poids supérieur dans celle qui suit.
 
Si l'intervalle de définition n'est pas encore suffisamment étendu, il faut faire appel au type Longlnt. Au niveau interne il correspond à un DWORD sur 4 octets interprété sous forme signée. L'intervalle de définition va de -2147483648 à 2147483647. Pour mémoriser les entiers Longlnt, Turbo Pascal suit les conventions posées par INTEL pour les mots doubles DWORD. Le mot de poids inférieur est mis dans la mémoire d'adresse basse, le mot de poids supérieur dans celle qui suit.
V Types énumérés
+
 
 +
===Types énumérés===
 +
 
 
Les types énumérés sont codés de façon que le premier élément reçoive le rang O, le deuxième le rang 1, et ainsi de suite. Tant que le nombre d'éléments est inférieur à 257, le type énuméré est géré sous la forme d'un octet BYTE. Si le nombre d'éléments est supérieur, le stockage utilise un entier WORD.
 
Les types énumérés sont codés de façon que le premier élément reçoive le rang O, le deuxième le rang 1, et ainsi de suite. Tant que le nombre d'éléments est inférieur à 257, le type énuméré est géré sous la forme d'un octet BYTE. Si le nombre d'éléments est supérieur, le stockage utilise un entier WORD.
V Booléens
+
 
 +
===Booléens===
 +
 
 
Au niveau interne, les variables booléennes sont stockées sous forme BYTE quoiqu'elles ne puissent prendre que les valeurs O (false) ou 1 (true). On peut se remémorer cette définition en considérant le type énuméré équivalent:
 
Au niveau interne, les variables booléennes sont stockées sous forme BYTE quoiqu'elles ne puissent prendre que les valeurs O (false) ou 1 (true). On peut se remémorer cette définition en considérant le type énuméré équivalent:
 +
 
type boolean - (VALSE. TRUE); I
 
type boolean - (VALSE. TRUE); I
 +
 
qui fournit les valeurs
 
qui fournit les valeurs
ord(FALSE) et ord(TRUE) I
+
 
 +
ord(FALSE) et ord(TRUE) I
 +
 
 
c'est-à-dire O ou 1.
 
c'est-à-dire O ou 1.
2-46 La Bible Turbo Pascal
+
 
2-47 Dans les coulisses du langage
+
===Caractères Char===
V Caractères Char
+
 
 
Les caractères sont mémorisés comme des octets BYTE (non signés).
 
Les caractères sont mémorisés comme des octets BYTE (non signés).
V Chaînes de caractères String
+
 
 +
===Chaînes de caractères String===
 
Les chaînes String sont des suites de caractères ordonnés. L'un des caractères contient la valeur de la longueur actuelle de la chaîne. Cette information ne doit pas être confondue avec la longueur déclarée de la chaîne qui n'est pas mémorisée à l'intérieur de celle-ci.
 
Les chaînes String sont des suites de caractères ordonnés. L'un des caractères contient la valeur de la longueur actuelle de la chaîne. Cette information ne doit pas être confondue avec la longueur déclarée de la chaîne qui n'est pas mémorisée à l'intérieur de celle-ci.
 +
 
Comme la longueur d'une chaîne occupe un octet, une chaîne ne peut pas dépasser 255 caractères: au maximum on aura donc 256 octets en incluant l'octet qui donne la longueur. Pour les chaînes déclarées sans longueur la mémoire réservée est toujours de 256 octets. Pour économiser de la mémoire, il vaut mieux indiquer une longueur maximale au moment de la déclaration lorsqu'on est en mesure de la prévoir.
 
Comme la longueur d'une chaîne occupe un octet, une chaîne ne peut pas dépasser 255 caractères: au maximum on aura donc 256 octets en incluant l'octet qui donne la longueur. Pour les chaînes déclarées sans longueur la mémoire réservée est toujours de 256 octets. Pour économiser de la mémoire, il vaut mieux indiquer une longueur maximale au moment de la déclaration lorsqu'on est en mesure de la prévoir.
 +
 
Une chaîne peut être adressée comme un tableau de caractères Char avec un indice compris entre O et la longueur de la chaîne. L'élément d'indice O renvoie la longueur actuelle de la chaîne, à l'indice I correspond le premier caractère de la chaîne. Ce format est très efficace et certains fabricants de logiciels (comme Microsoft) l'utilisent même avec le langage C, alors même que ce langage prévoit une autre représentation!
 
Une chaîne peut être adressée comme un tableau de caractères Char avec un indice compris entre O et la longueur de la chaîne. L'élément d'indice O renvoie la longueur actuelle de la chaîne, à l'indice I correspond le premier caractère de la chaîne. Ce format est très efficace et certains fabricants de logiciels (comme Microsoft) l'utilisent même avec le langage C, alors même que ce langage prévoit une autre représentation!
 +
 
La possibilité d'accéder directement à l'octet définissant la longueur de la chaîne permet quelques astuces de manipulation pour accélérer l'exécution d'un programme. Le listing suivant montre quelques-unes des techniques envisageables:
 
La possibilité d'accéder directement à l'octet définissant la longueur de la chaîne permet quelques astuces de manipulation pour accélérer l'exécution d'un programme. Le listing suivant montre quelques-unes des techniques envisageables:
 +
 
Program Astuces;.J
 
Program Astuces;.J
 
var chaine : string; f Chaine de long. max. 255 caractères )J
 
var chaine : string; f Chaine de long. max. 255 caractères )J
Ligne 263 : Ligne 290 :
 
writeln(chalne);.J
 
writeln(chalne);.J
 
end.
 
end.
 +
 
Notez bien que vous ne pouvez pas utiliser directement la longueur de la chaîne comme indice, car du point de vue de Turbo Pascal cette longueur est constituée par un caractère, c'est-à-dire un type de donnée impropre à indicer une chaîne. Mais la fonction Ord permet de transformer un caractère Char en un octet Byte qui est, quant à lui, susceptible de servir d'indice.
 
Notez bien que vous ne pouvez pas utiliser directement la longueur de la chaîne comme indice, car du point de vue de Turbo Pascal cette longueur est constituée par un caractère, c'est-à-dire un type de donnée impropre à indicer une chaîne. Mais la fonction Ord permet de transformer un caractère Char en un octet Byte qui est, quant à lui, susceptible de servir d'indice.
V Tableaux
+
 
 +
===Tableaux===
 +
 
 
Les tableaux sont stockés sous la forme de leurs éléments juxtaposés. L'élément dont l'indice est le plus petit est rangé dans la mémoire dont l'adresse est la plus basse. Il n'y a pas d'espace entre les éléments qui sont mis bout à bout.
 
Les tableaux sont stockés sous la forme de leurs éléments juxtaposés. L'élément dont l'indice est le plus petit est rangé dans la mémoire dont l'adresse est la plus basse. Il n'y a pas d'espace entre les éléments qui sont mis bout à bout.
 +
 
Dans les tableaux multidimensionnels ce sont les éléments de la dernière dimension qui se retrouvent côte à côte. Ainsi le tableau
 
Dans les tableaux multidimensionnels ce sont les éléments de la dernière dimension qui se retrouvent côte à côte. Ainsi le tableau
Dans les coulisses du langage 2-47
+
 
La Bible Turbo Pascal 2-48
+
a—array[1. .1O][1. .5] 0f char I
a—array[1. .1O][1. .5] 0f char I
+
 
 
présente en mémoire la disposition suivante:
 
présente en mémoire la disposition suivante:
 
d'abord les éléments a[1][1] à a[1][5], puis les éléments a[2][1] à a[2][51, et ainsi de suite.
 
d'abord les éléments a[1][1] à a[1][5], puis les éléments a[2][1] à a[2][51, et ainsi de suite.
V Enregistrements Record
+
 
 +
===Enregistrements Record===
 +
 
 
Du point de vue du stockage, Turbo Pascal traite les enregistrements comme les éléments d'un tableau. Les différents champs sont donc mis bout à bout dans l'ordre de leur déclaration. Le premier élément se trouve à l'adresse la plus basse, le dernier à l'adresse la plus haute. A l'intérieur d'un enregistrement les parties variables (introduites par CASE) se recouvrent.
 
Du point de vue du stockage, Turbo Pascal traite les enregistrements comme les éléments d'un tableau. Les différents champs sont donc mis bout à bout dans l'ordre de leur déclaration. Le premier élément se trouve à l'adresse la plus basse, le dernier à l'adresse la plus haute. A l'intérieur d'un enregistrement les parties variables (introduites par CASE) se recouvrent.
V Ensembles
+
 
 +
===Ensembles====
 
Toujours soucieux de consommer le moins de mémoire possible, Turbo Pascal range les ensembles sous forme de tableaux de bits, chaque bit représentant un élément de l'ensemble. Si le bit est à un, l'élément appartient à l'ensemble; si le bit est à zéro, il n'en fait pas partie. La place mémoire utilisée dépend du nombre des éléments et par conséquent du rang ordinal du dernier et du premier élément (appelés ici Omin et Omax), selon la formule:
 
Toujours soucieux de consommer le moins de mémoire possible, Turbo Pascal range les ensembles sous forme de tableaux de bits, chaque bit représentant un élément de l'ensemble. Si le bit est à un, l'élément appartient à l'ensemble; si le bit est à zéro, il n'en fait pas partie. La place mémoire utilisée dépend du nombre des éléments et par conséquent du rang ordinal du dernier et du premier élément (appelés ici Omin et Omax), selon la formule:
 
Nb octets - (Omax div 8) - (Omin div 8) + 1
 
Nb octets - (Omax div 8) - (Omin div 8) + 1
V Pointeurs
+
===Pointeurs===
 
En Turbo Pascal les pointeurs sont essentiellement de type FAR. Ils se composent donc d'une adresse de segment et d'un déplacement qui sont tous deux dans le format WORD. Selon la convention prescrite par INTEL, le déplacement (offset) précède l'adresse du segment en mémoire centrale.
 
En Turbo Pascal les pointeurs sont essentiellement de type FAR. Ils se composent donc d'une adresse de segment et d'un déplacement qui sont tous deux dans le format WORD. Selon la convention prescrite par INTEL, le déplacement (offset) précède l'adresse du segment en mémoire centrale.
 +
 
Pour accéder aux variables globales et aux constantes typées du segment de données, Turbo Pascal pourrait également mettre en service des pointeurs NEAR, puisque la zone mémoire concernée ne dépasse pas 64 Ko et qu'elle peut être adressée en permanence par le registre DS. Mais dans ce cas, le tas (heap) deviendrait inaccessible à ces pointeurs. Or le tas joue un rôle important dans la programmation en Turbo Pascal et il n'est pas concevable qu'une distinction s'opère à ce niveau entre les pointeurs de données ordinaires et les pointeurs de tas. C'est la raison pour laquelle Turbo Pascal utilise les pointeurs FAR. Toute la mémoire de I Mo disponible sur un PC est ainsi accessible.
 
Pour accéder aux variables globales et aux constantes typées du segment de données, Turbo Pascal pourrait également mettre en service des pointeurs NEAR, puisque la zone mémoire concernée ne dépasse pas 64 Ko et qu'elle peut être adressée en permanence par le registre DS. Mais dans ce cas, le tas (heap) deviendrait inaccessible à ces pointeurs. Or le tas joue un rôle important dans la programmation en Turbo Pascal et il n'est pas concevable qu'une distinction s'opère à ce niveau entre les pointeurs de données ordinaires et les pointeurs de tas. C'est la raison pour laquelle Turbo Pascal utilise les pointeurs FAR. Toute la mémoire de I Mo disponible sur un PC est ainsi accessible.
 
Le pointeur appelé NIL joue un rôle particulier. L'adresse de son segment et son déplacement valent tous deux O. Il pointe donc sur la première entrée de la table des vecteurs d'interruption et ne doit pas être utilisé pour référencer des données.
 
Le pointeur appelé NIL joue un rôle particulier. L'adresse de son segment et son déplacement valent tous deux O. Il pointe donc sur la première entrée de la table des vecteurs d'interruption et ne doit pas être utilisé pour référencer des données.
 +
 
Les pointeurs de procédures et de fonctions, c'est-à-dire les types procéduraux, sont également stockés sous la forme FAR. Ici il n'y a pas d'alternative possible car tout
 
Les pointeurs de procédures et de fonctions, c'est-à-dire les types procéduraux, sont également stockés sous la forme FAR. Ici il n'y a pas d'alternative possible car tout
 
2-48 La Bible Turbo Pascal
 
2-48 La Bible Turbo Pascal
Ligne 288 : Ligne 324 :
 
Pour manier les nombres en virgule flottante, le Turbo Pascal standard ne connaît que le type Real. Mais si on met en service un coprocesseur arithmétique ou une bibliothèque d'émulation, on peut utiliser trois autres types de données qui fonctionnent avec davantage de précision.
 
Pour manier les nombres en virgule flottante, le Turbo Pascal standard ne connaît que le type Real. Mais si on met en service un coprocesseur arithmétique ou une bibliothèque d'émulation, on peut utiliser trois autres types de données qui fonctionnent avec davantage de précision.
 
Je ne voudrais pas m'étendre sur les formats internes de ces types, car leur étude est très ardue. Les règles de stockage et de combinaison des réels sont plutôt du domaine d'un cours de "mathématiques numériques". Il est toutefois important de se souvenir que le type standard Real est mémorisé sur six octets.
 
Je ne voudrais pas m'étendre sur les formats internes de ces types, car leur étude est très ardue. Les règles de stockage et de combinaison des réels sont plutôt du domaine d'un cours de "mathématiques numériques". Il est toutefois important de se souvenir que le type standard Real est mémorisé sur six octets.
V Alignement des variables
+
 
 +
===Alignement des variables===
 +
 
 
Avec la technique d'alignement des variables on cherche à influencer le rangement des variables de façon à les rendre plus rapidement accessibles. Il faut se rappeler à ce sujet que le 8086 et ses successeurs ont plus de facilité à accéder à des variables de type WORD et DWORD (en Pascal: Word et Longlnt) lorsqu'elles sont stockées à des adresses paires. L'accès aux adresses impaires est plus lent parce que dans ce cas le processeur doit exécuter plusieurs lectures pour charger ou stocker les différents octets qui composent un opérande. Il faut une coordination spéciale entre le processeur et le bus de données à 16 bits qui relie le processeur à la mémoire centrale. Les systèmes pourvus d'un 8088 ne tirent aucun profit des techniques d'alignement car ce processeur ne travaille qu'avec un bus à 8 bits. Ainsi les variables traitées, qu'elles soient de format WORD ou DWORD, sont de toute façon découpées en octets.
 
Avec la technique d'alignement des variables on cherche à influencer le rangement des variables de façon à les rendre plus rapidement accessibles. Il faut se rappeler à ce sujet que le 8086 et ses successeurs ont plus de facilité à accéder à des variables de type WORD et DWORD (en Pascal: Word et Longlnt) lorsqu'elles sont stockées à des adresses paires. L'accès aux adresses impaires est plus lent parce que dans ce cas le processeur doit exécuter plusieurs lectures pour charger ou stocker les différents octets qui composent un opérande. Il faut une coordination spéciale entre le processeur et le bus de données à 16 bits qui relie le processeur à la mémoire centrale. Les systèmes pourvus d'un 8088 ne tirent aucun profit des techniques d'alignement car ce processeur ne travaille qu'avec un bus à 8 bits. Ainsi les variables traitées, qu'elles soient de format WORD ou DWORD, sont de toute façon découpées en octets.
 +
 
L'alignement des variables est commandé par l'option "Options/Compiler/Align Data" du menu "Options". On peut alors choisir entre les modes BYTE et WORD. En mode BYTE, aucun alignement n'est effectué. Chaque variable est simplement disposée à la suite de la précédente. En mode WORD, toutes les variables qui occupent deux ou quatre octets sont décalées vers une adresse paire lorsque les circonstances naturelles les feraient arriver à une adresse impaire. Chaque fois que ce décalage est appliqué, un espace libre d'un octet sépare la nouvelle variable de la précédente.
 
L'alignement des variables est commandé par l'option "Options/Compiler/Align Data" du menu "Options". On peut alors choisir entre les modes BYTE et WORD. En mode BYTE, aucun alignement n'est effectué. Chaque variable est simplement disposée à la suite de la précédente. En mode WORD, toutes les variables qui occupent deux ou quatre octets sont décalées vers une adresse paire lorsque les circonstances naturelles les feraient arriver à une adresse impaire. Chaque fois que ce décalage est appliqué, un espace libre d'un octet sépare la nouvelle variable de la précédente.
 +
 
L'alignement peut aussi être commandé de l'intérieur d'un programme par la directive de compilation i$A+) / ($A-). En activant ou en désactivant la directive, on peut aligner sélectivement un groupe de variable dans une portion de programme. Les membres des structures ne sont pas touchés, car ils sont toujours mémorisés de façon contiguë pour ne pas gaspiller de place mémoire. La même remarque s'applique aux éléments des tableaux.
 
L'alignement peut aussi être commandé de l'intérieur d'un programme par la directive de compilation i$A+) / ($A-). En activant ou en désactivant la directive, on peut aligner sélectivement un groupe de variable dans une portion de programme. Les membres des structures ne sont pas touchés, car ils sont toujours mémorisés de façon contiguë pour ne pas gaspiller de place mémoire. La même remarque s'applique aux éléments des tableaux.
 +
 
Le programme suivant montre comment le mode d'alignement influence le rangement des variables. Compilez et exécutez d'abord ce programme avec le mode d'alignement WORD. Puis refaites les mêmes opérations avec le mode BYTE. Vous verrez comment Turbo Pascal déplace les variables.
 
Le programme suivant montre comment le mode d'alignement influence le rangement des variables. Compilez et exécutez d'abord ce programme avec le mode d'alignement WORD. Puis refaites les mêmes opérations avec le mode BYTE. Vous verrez comment Turbo Pascal déplace les variables.
 
Dans les coulisses du langage 2-49
 
Dans les coulisses du langage 2-49
Ligne 322 : Ligne 363 :
 
writeln('s.li: not(odd(ofs( s.li ))) );J
 
writeln('s.li: not(odd(ofs( s.li ))) );J
 
end.
 
end.
2.4. Procédures et fonctions
+
 
 +
==Procédures et fonctions==
 +
 
 
NEAR ouFAR 7 ...................................................50 Le cas le plus simple: une procédure sans paramètre ni variable locale .. .52
 
NEAR ouFAR 7 ...................................................50 Le cas le plus simple: une procédure sans paramètre ni variable locale .. .52
 
Variables locales ..................................................53
 
Variables locales ..................................................53
 
Transmission des paramètres ........................................56 Fonctions.........................................................63 Récursion.........................................................69
 
Transmission des paramètres ........................................56 Fonctions.........................................................63 Récursion.........................................................69
 +
 
Les procédures et les fonctions sont les principales composantes des langages structurés: elles ne doivent donc pas être absentes de ce chapitre. Nous étudierons comment Turbo Pascal stocke et appelle les procédures et les fonctions, comment sont gérées les variables locales et les paramètres.
 
Les procédures et les fonctions sont les principales composantes des langages structurés: elles ne doivent donc pas être absentes de ce chapitre. Nous étudierons comment Turbo Pascal stocke et appelle les procédures et les fonctions, comment sont gérées les variables locales et les paramètres.
V NEAR ou FAR?
+
 
 +
===NEAR ou FAR?===
 +
 
 
A l'inverse de Turbo Pascal, le processeur ne fait pas de distinction entre procédures et fonctions. Pour lui il n'existe que des sous-programmes appelés procédures et parmi celles-ci il distingue le type FAR du type NEAR.
 
A l'inverse de Turbo Pascal, le processeur ne fait pas de distinction entre procédures et fonctions. Pour lui il n'existe que des sous-programmes appelés procédures et parmi celles-ci il distingue le type FAR du type NEAR.
 +
 
Cette différence ne se manifeste qu'au moment de la dernière instruction du sous-programme, c'est-à-dire lors du retour au programme appelant. L'instruction correspondante porte toujours le nom RET (Return), suffixé tantôt par N (NEAR),
 
Cette différence ne se manifeste qu'au moment de la dernière instruction du sous-programme, c'est-à-dire lors du retour au programme appelant. L'instruction correspondante porte toujours le nom RET (Return), suffixé tantôt par N (NEAR),
2-50 La Bible Turbo Pascal
 
2-51 Dans les coulisses du langage
 
 
tantôt par F (FAR). Elle dépile l'adresse empilée précédemment par un ordre CALL et y poursuit l'exécution du programme.
 
tantôt par F (FAR). Elle dépile l'adresse empilée précédemment par un ordre CALL et y poursuit l'exécution du programme.
 +
 
L'instruction RETN se contente de prélever dans la pile une adresse de déplacement qui est aussitôt transférée dans le registre IP (compteur ordinal).
 
L'instruction RETN se contente de prélever dans la pile une adresse de déplacement qui est aussitôt transférée dans le registre IP (compteur ordinal).
 +
 
Le retour s'effectue donc vers un programme appelant qui se trouve dans le même segment de code que le sous-programme appelé.
 
Le retour s'effectue donc vers un programme appelant qui se trouve dans le même segment de code que le sous-programme appelé.
 +
 
L'instruction RETF permet par contre de revenir à un programme appelant qui se trouve dans un autre segment de code que le sous-programme appelé. En effet, RETF ne dépile pas seulement un déplacement mais aussi une adresse de segment qui est transférée dans le registre CS.
 
L'instruction RETF permet par contre de revenir à un programme appelant qui se trouve dans un autre segment de code que le sous-programme appelé. En effet, RETF ne dépile pas seulement un déplacement mais aussi une adresse de segment qui est transférée dans le registre CS.
 +
 
Aux deux instructions RET mentionnées correspondent deux instructions d'appel CALL. L'appel NEAR (NEAR Call) sert à appeler des sous-programmes qui se trouvent dans le même segment de code tandis que l'appel FAR (FAR Cail) sert à appeler des sous-programmes qui se trouvent dans d'autres segments de code. Les appels et les retours FAR consommant plus de temps machine que leurs homologues NEAR, un programme efficace s'appliquera à ne mettre en service des ordres FAR que dans la limite de l'indispensable.
 
Aux deux instructions RET mentionnées correspondent deux instructions d'appel CALL. L'appel NEAR (NEAR Call) sert à appeler des sous-programmes qui se trouvent dans le même segment de code tandis que l'appel FAR (FAR Cail) sert à appeler des sous-programmes qui se trouvent dans d'autres segments de code. Les appels et les retours FAR consommant plus de temps machine que leurs homologues NEAR, un programme efficace s'appliquera à ne mettre en service des ordres FAR que dans la limite de l'indispensable.
 +
 
Il est ainsi, bien sûr, de Turbo Pascal. Les concepteurs de la maison Borland ont développé un mécanisme raffiné d'attribution des modes NEAR et FAR.
 
Il est ainsi, bien sûr, de Turbo Pascal. Les concepteurs de la maison Borland ont développé un mécanisme raffiné d'attribution des modes NEAR et FAR.
les procédures et fonctions du programme principal sont essentiellement NEAR. car elles se trouvent toutes dans le même segment de code et s'appellent mutuellement.
+
* les procédures et fonctions du programme principal sont essentiellement NEAR. car elles se trouvent toutes dans le même segment de code et s'appellent mutuellement.
les procédures et fonctions déclarées à l'intérieur d'une procédure ou d'une fonction sont également NEAR Etant appelées à un niveau local, elles peuvent être placées à l'intérieur du segment de code du programme appelant.
+
* les procédures et fonctions déclarées à l'intérieur d'une procédure ou d'une fonction sont également NEAR Etant appelées à un niveau local, elles peuvent être placées à l'intérieur du segment de code du programme appelant.
les procédures et fonctions qui sont mentionnées dans la partie Interface d'une unité sont FAR, car elles sont susceptibles d'être appelées à partir d'autres unités ou à partir du programme principal, c'est-à-dire à partir d'autres segments de code.
+
* les procédures et fonctions qui sont mentionnées dans la partie Interface d'une unité sont FAR, car elles sont susceptibles d'être appelées à partir d'autres unités ou à partir du programme principal, c'est-à-dire à partir d'autres segments de code.
les procédures et fonctions qui ne sont mentionnées que dans la partie Implémentation d'une unité sont NEAR. On ne peut en effet y faire appel que de l'intérieur de l'unité, c'est-à-dire de l'intérieur du même segment de code.
+
* les procédures et fonctions qui ne sont mentionnées que dans la partie Implémentation d'une unité sont NEAR. On ne peut en effet y faire appel que de l'intérieur de l'unité, c'est-à-dire de l'intérieur du même segment de code.
 +
 
 
Le type de la procédure ou de la fonction mise en service correspond en général au type d'appel NEAR ou CALL généré, sauf dans un cas particulier. Ce cas est celui où à l'intérieur d'une unité on fait appel à des routines mentionnées dans la partie Implémentation et de ce fait classifiées comme FAR. Ces routines devraient théoriquement être invoquées par un appel FAR car elles se terminent par une instruction RETF. Si on utilise un appel NEAR, le système se plante inévitablement
 
Le type de la procédure ou de la fonction mise en service correspond en général au type d'appel NEAR ou CALL généré, sauf dans un cas particulier. Ce cas est celui où à l'intérieur d'une unité on fait appel à des routines mentionnées dans la partie Implémentation et de ce fait classifiées comme FAR. Ces routines devraient théoriquement être invoquées par un appel FAR car elles se terminent par une instruction RETF. Si on utilise un appel NEAR, le système se plante inévitablement
Dans les coulisses du langage 2-51
 
La Bible Turbo Pascal 2-52
 
 
au moment du retour car l'adresse de segment n'a pas été empilée. Mais plutôt que d'utiliser un appel FAR, Turbo Pascal préfère dans ce cas mettre en service une méthode plus efficace qui se traduit par les instructions:
 
au moment du retour car l'adresse de segment n'a pas été empilée. Mais plutôt que d'utiliser un appel FAR, Turbo Pascal préfère dans ce cas mettre en service une méthode plus efficace qui se traduit par les instructions:
push cs
+
 
call near routine
+
push cs
 +
call near routine
 +
 
 
Cette séquence simule un appel FAR en empilant la valeur actuelle du segment de code avant de procéder à un appel NEAR standard. L'instruction de retour RETF qui clôture la routine appelée trouve alors sur la pile l'adresse qu'elle cherche. En même temps, quelques précieuses microsecondes sont gagnées par rapport à l'exécution d'un appel FAR complet.
 
Cette séquence simule un appel FAR en empilant la valeur actuelle du segment de code avant de procéder à un appel NEAR standard. L'instruction de retour RETF qui clôture la routine appelée trouve alors sur la pile l'adresse qu'elle cherche. En même temps, quelques précieuses microsecondes sont gagnées par rapport à l'exécution d'un appel FAR complet.
 +
 
Le programmeur peut introduire des exceptions aux règles générales précédemment exposées. Il lui suffit d'utiliser la directive de compilation {$F+) qui provoque la génération de procédures et de fonctions en mode FAR. Cette mesure n'a de sens que si vous appelez les procédures/ fonctions par des pointeurs, à moins que vous ne désiriez essayer quelques astuces de programmation, notamment en liaison avec l'assembleur. Vous trouverez des exemples de ce type dans les chapitres suivants.
 
Le programmeur peut introduire des exceptions aux règles générales précédemment exposées. Il lui suffit d'utiliser la directive de compilation {$F+) qui provoque la génération de procédures et de fonctions en mode FAR. Cette mesure n'a de sens que si vous appelez les procédures/ fonctions par des pointeurs, à moins que vous ne désiriez essayer quelques astuces de programmation, notamment en liaison avec l'assembleur. Vous trouverez des exemples de ce type dans les chapitres suivants.
 
Le cas le plus simple : une procédure sans paramètre ni variable locale.
 
Le cas le plus simple : une procédure sans paramètre ni variable locale.
 +
 
Nous allons examiner le cas le plus simple: une procédure sans paramètre ni variable locale. Le listing suivant montre comment une telle procédure est transformée en code machine et assembleur.
 
Nous allons examiner le cas le plus simple: une procédure sans paramètre ni variable locale. Le listing suivant montre comment une telle procédure est transformée en code machine et assembleur.
 
program p_std;.J .1 ;Empile BP
 
program p_std;.J .1 ;Empile BP
Ligne 371 : Ligne 424 :
 
cs:000F E8EEFF call TEST ;Appel NEAR à test.J
 
cs:000F E8EEFF call TEST ;Appel NEAR à test.J
 
end.
 
end.
 +
 
Vous voyez que la procédure test ci-dessus n'effectue aucune tâché. Elle se compose simplement d'un couple d'instructions Begin/End. On pourrait penser que le code généré se limite à une instruction RET. Mais que fait donc Turbo Pascal? Il fabrique quatre instructions supplémentaires qui manipulent de façon quelque peu obscure les registres BP et SP. En fait, ces instructions permettent de travailler avec les variables locales et d'accéder aux paramètres transmis. Elles réapparaîtront dans les exemples
 
Vous voyez que la procédure test ci-dessus n'effectue aucune tâché. Elle se compose simplement d'un couple d'instructions Begin/End. On pourrait penser que le code généré se limite à une instruction RET. Mais que fait donc Turbo Pascal? Il fabrique quatre instructions supplémentaires qui manipulent de façon quelque peu obscure les registres BP et SP. En fait, ces instructions permettent de travailler avec les variables locales et d'accéder aux paramètres transmis. Elles réapparaîtront dans les exemples
2-52 La Bible Turbo Pascal
 
2-53 Dans les coulisses du langage
 
 
ultérieurs. Il est simplement dommage que Turbo Pascal les introduise alors même qu'elles sont inutiles.
 
ultérieurs. Il est simplement dommage que Turbo Pascal les introduise alors même qu'elles sont inutiles.
 +
 
Note: les quatre instructions ne consomment que 22 cycles machine,
 
Note: les quatre instructions ne consomment que 22 cycles machine,
 
c'est-à-dire 2,6 microsecondes sur un AT à 8 MHz. Mais de telles inattentions réduisent à néant d'autres optimisations infiniment plus raffinées.
 
c'est-à-dire 2,6 microsecondes sur un AT à 8 MHz. Mais de telles inattentions réduisent à néant d'autres optimisations infiniment plus raffinées.
V Variables locales
+
 
 +
===Variables locales===
 +
 
 
Partant de l'exemple précédent, nous avons introduit dans la procédure test trois variables locales. Leur format interne est identique à celui des variables globales mais l'endroit où elles sont mémorisées est différent. Examinons le listing suivant:
 
Partant de l'exemple précédent, nous avons introduit dans la procédure test trois variables locales. Leur format interne est identique à celui des variables globales mais l'endroit où elles sont mémorisées est différent. Examinons le listing suivant:
 +
 
program p_varloc;.J J
 
program p_varloc;.J J
 
procedure test;.J -J
 
procedure test;.J -J
Ligne 420 : Ligne 476 :
 
end
 
end
 
Au début de test, le contenu du registre BP est sauvegardé sur la pile. Dans l'exemple précédent cette disposition n'avait pas d'utilité: mais maintenant elle est importante en raison de l'ordre suivant qui modifie le contenu de BP. Le contenu du registre SP est mis dans BP, ce qui permet à BP de pointer au même endroit que SP. Mais les deux registres ne vont pas rester identiques très longtemps. Car l'instruction suivante
 
Au début de test, le contenu du registre BP est sauvegardé sur la pile. Dans l'exemple précédent cette disposition n'avait pas d'utilité: mais maintenant elle est importante en raison de l'ordre suivant qui modifie le contenu de BP. Le contenu du registre SP est mis dans BP, ce qui permet à BP de pointer au même endroit que SP. Mais les deux registres ne vont pas rester identiques très longtemps. Car l'instruction suivante
Dans les coulisses du langage 2-53
 
La Bible Turbo Pascal 2-54
 
 
soustrait déjà la valeur 260 à SP. Il s'agit là d'une étape décisive pour la gestion des variables locales, car la pile contient désormais un espace libre de 259 octets. C'est dans cet espace que Turbo Pascal va mémoriser les variables locales de la procédure. On pourrait pensera priori que la pile n'est pas un endroit de tout repos pour stocker des variables. Mais après mûre réflexion, il apparaît au contraire qu'il s'agit là d'un endroit tout à fait idéal.
 
soustrait déjà la valeur 260 à SP. Il s'agit là d'une étape décisive pour la gestion des variables locales, car la pile contient désormais un espace libre de 259 octets. C'est dans cet espace que Turbo Pascal va mémoriser les variables locales de la procédure. On pourrait pensera priori que la pile n'est pas un endroit de tout repos pour stocker des variables. Mais après mûre réflexion, il apparaît au contraire qu'il s'agit là d'un endroit tout à fait idéal.
 +
 
D'une manière générale, les informations stockées sur la pile n'y restent pas longtemps c'est ce que nous a montré par exemple la gestion de l'adresse de retour associée à une instruction CALL. Chaque fois qu'un sous-programme rend la main au programme appelant, l'information constituée par l'adresse de retour devient inaccessible. Elle est même écrasée dès qu'on empile une nouvelle donnée. La pile fonctionne donc selon une certaine dynamique, et c'est cette dynamique qui précisément la prédestine au stockage des variables locales.
 
D'une manière générale, les informations stockées sur la pile n'y restent pas longtemps c'est ce que nous a montré par exemple la gestion de l'adresse de retour associée à une instruction CALL. Chaque fois qu'un sous-programme rend la main au programme appelant, l'information constituée par l'adresse de retour devient inaccessible. Elle est même écrasée dès qu'on empile une nouvelle donnée. La pile fonctionne donc selon une certaine dynamique, et c'est cette dynamique qui précisément la prédestine au stockage des variables locales.
 +
 
Car les variables locales - tout comme les adresses de retour - n'ont qu'une durée de vie limitée à l'exécution de la fonction ou de la procédure. Il serait évidemment possible de disposer les variables locales dans le segment de données mais ce segment se révélerait vite trop petit pour l'ensemble des variables généralement utilisées dans un programme. Il faudrait en fait choisir un espace de recouvrement uniquement réservé aux variables locales, qui ne serait occupé à un moment donné que par celles qui appartiennent à la fonction ou la procédure en cours d'exécution. Cet espace serait juste assez vaste pour contenir les variables locales de la fonction ou de la procédure qui en met en oeuvre la plus grande quantité. Mais ce système ne fonctionnerait pas car les procédures et les fonctions peuvent s'appeler réciproquement dans l'espace de recouvrement les variables de l'une écraseraient les variables de l'autre!
 
Car les variables locales - tout comme les adresses de retour - n'ont qu'une durée de vie limitée à l'exécution de la fonction ou de la procédure. Il serait évidemment possible de disposer les variables locales dans le segment de données mais ce segment se révélerait vite trop petit pour l'ensemble des variables généralement utilisées dans un programme. Il faudrait en fait choisir un espace de recouvrement uniquement réservé aux variables locales, qui ne serait occupé à un moment donné que par celles qui appartiennent à la fonction ou la procédure en cours d'exécution. Cet espace serait juste assez vaste pour contenir les variables locales de la fonction ou de la procédure qui en met en oeuvre la plus grande quantité. Mais ce système ne fonctionnerait pas car les procédures et les fonctions peuvent s'appeler réciproquement dans l'espace de recouvrement les variables de l'une écraseraient les variables de l'autre!
PUSHBP SUBSP,$104 MOVSP,BP
+
 
MOV BS,SP POP BP
+
Muses SUU
+
Pdrsues
+
d.r.I, de Foi" d.Lz d,r,tow
+
SP BP
+
BP
+
—[BP al
+
'—[BP 2
+
B
+
+
S255
+
S254 (n
+
S253
+
SC)
+
+
+
5(0)
+
IBP 259) CD
+
u,
+
:.:..
+
.0m 8S.Offl SSIOM 100W
+
+
+
9.Ibre
+
Figure 17: Gestion des variables locales sur la pile
+
2-54 La Bible Turbo Pascal
+
2-55 Dans les coulisses du langage
+
 
En plaçant le pointeur de pile au-delà de la zone de ses variables locales, la procédure test met cette zone à l'abri des stockages qui pourraient en écraser les données. Ainsi en cas d'appel à un sous-programme -qui empile l'adresse de retour et introduit ses propres variables locales- la zone reste intacte. Chaque fois qu'une procédure ou une fonction est appelée, elle déplace le pointeur de pile derrière ses variables locales pour les protéger contre toute agression ennemie. Le pointeur de pile descend ainsi vers le début de la pile en mémoire centrale, s'éloignant des variables locales préservées.
 
En plaçant le pointeur de pile au-delà de la zone de ses variables locales, la procédure test met cette zone à l'abri des stockages qui pourraient en écraser les données. Ainsi en cas d'appel à un sous-programme -qui empile l'adresse de retour et introduit ses propres variables locales- la zone reste intacte. Chaque fois qu'une procédure ou une fonction est appelée, elle déplace le pointeur de pile derrière ses variables locales pour les protéger contre toute agression ennemie. Le pointeur de pile descend ainsi vers le début de la pile en mémoire centrale, s'éloignant des variables locales préservées.
 +
 
Bien entendu, à la fin de l'exécution d'une procédure ou d'une fonction, le pointeur de pile doit se retrouver dans la position qu'il avait au début de l'exécution. Les instructions appropriées sont mises en place par Turbo Pascal. C'est d'abord le registre SP qui est chargé avec le contenu de BP. II retrouve ainsi sa valeur d'origine et se déplace par-dessus les variables locales vers la fin de la pile. II pointe à nouveau sur l'endroit où le contenu original du registre BP avait été sauvegardé par le PUSH d'introduction. Ce contenu est ensuite repris par l'instruction POP et le registre BP retrouve lui aussi sa valeur d'origine.
 
Bien entendu, à la fin de l'exécution d'une procédure ou d'une fonction, le pointeur de pile doit se retrouver dans la position qu'il avait au début de l'exécution. Les instructions appropriées sont mises en place par Turbo Pascal. C'est d'abord le registre SP qui est chargé avec le contenu de BP. II retrouve ainsi sa valeur d'origine et se déplace par-dessus les variables locales vers la fin de la pile. II pointe à nouveau sur l'endroit où le contenu original du registre BP avait été sauvegardé par le PUSH d'introduction. Ce contenu est ensuite repris par l'instruction POP et le registre BP retrouve lui aussi sa valeur d'origine.
 +
 
Le registre SP pointe maintenant sur l'adresse de retour au programme appelant: l'instruction RET clôture le sous-programme et confie la poursuite de l'exécution au programme appelant.
 
Le registre SP pointe maintenant sur l'adresse de retour au programme appelant: l'instruction RET clôture le sous-programme et confie la poursuite de l'exécution au programme appelant.
 
On pourrait penser que seul le registre SP sert à gérer les variables locales: à quoi bon alors manipuler le registre BP ? En fait, le processeur a prévu le registre BP pour l'adressage indexé de la pile: quand on utilise ce registre, c'est le registre SS -au lieu de DS- qui intervient pour former l'adresse complète. Il en résulte que BP est indispensable pour adresser les variables locales à l'aide de la pile.
 
On pourrait penser que seul le registre SP sert à gérer les variables locales: à quoi bon alors manipuler le registre BP ? En fait, le processeur a prévu le registre BP pour l'adressage indexé de la pile: quand on utilise ce registre, c'est le registre SS -au lieu de DS- qui intervient pour former l'adresse complète. Il en résulte que BP est indispensable pour adresser les variables locales à l'aide de la pile.
 +
 
Les instructions disposées entre le début et la fin de la procédure illustrent clairement ce mécanisme. Si on examine le code machine généré, on constate que I commence à l'adresse [BP-$2], B en [BP-$31 et la chaîne S en [BP-$103]. Ces adresses n'ont pas été choisies au hasard, mais elles découlent de la taille respective des variables et de l'ordre dans lequel ces variables sont déclarées dans la procédure.
 
Les instructions disposées entre le début et la fin de la procédure illustrent clairement ce mécanisme. Si on examine le code machine généré, on constate que I commence à l'adresse [BP-$2], B en [BP-$31 et la chaîne S en [BP-$103]. Ces adresses n'ont pas été choisies au hasard, mais elles découlent de la taille respective des variables et de l'ordre dans lequel ces variables sont déclarées dans la procédure.
 +
 
C'est la variable I qui est créée en premier: elle prend 2 octets. A l'adresse [BP-01 se trouve encore l'octet inférieur de l'ancien contenu de BP: il ne faut donc pas que les variables locales dépassent l'adresse [BP-1]. Pour cette raison I commence en IBP-21 et se termine exactement en [BP-11. La variable B est disposée juste après I. Comme elle n'occupe qu'un octet, elle est rangée sur la pile sous forme de BYTE en [BP-3]. La dernière variable locale est la chaîne S. Elle occupe 256 octets et doit donc commencer 256 octets derrière la variable B, ce qui correspond à l'adresse [BP-259].
 
C'est la variable I qui est créée en premier: elle prend 2 octets. A l'adresse [BP-01 se trouve encore l'octet inférieur de l'ancien contenu de BP: il ne faut donc pas que les variables locales dépassent l'adresse [BP-1]. Pour cette raison I commence en IBP-21 et se termine exactement en [BP-11. La variable B est disposée juste après I. Comme elle n'occupe qu'un octet, elle est rangée sur la pile sous forme de BYTE en [BP-3]. La dernière variable locale est la chaîne S. Elle occupe 256 octets et doit donc commencer 256 octets derrière la variable B, ce qui correspond à l'adresse [BP-259].
 +
 
Chaque procédure ou fonction se comportant de la façon décrite plus haut, il est possible d'imbriquer les appels pratiquement sans limite, aucune procédure ou fonction ne pouvant empiéter sur la sphère privée de l'autre.
 
Chaque procédure ou fonction se comportant de la façon décrite plus haut, il est possible d'imbriquer les appels pratiquement sans limite, aucune procédure ou fonction ne pouvant empiéter sur la sphère privée de l'autre.
 +
 
Turbo Pascal n'est pas le seul langage à exploiter la pile pour gérer les variables locales. Tous les langages évolués se servent de cette technique qui est d'ailleurs elle-même soutenue par les instructions et l'architecture du processeur, par le moyen de l'adressage indexé selon BP.
 
Turbo Pascal n'est pas le seul langage à exploiter la pile pour gérer les variables locales. Tous les langages évolués se servent de cette technique qui est d'ailleurs elle-même soutenue par les instructions et l'architecture du processeur, par le moyen de l'adressage indexé selon BP.
Dans les coulisses du langage 2-55
+
 
La Bible Turbo Pascal 2-56
+
===Transmission des paramètres===
V Transmission des paramètres
+
 
Les paramètres des procédures et des fonctions sont également transmis par l'intermédiaire de la pile qui reçoit une à une les valeurs correspondantes. La transmission se fait de gauche à droite, de sorte que le déplacement par rapport au pointeur de pile augmente également de gauche à droite.
 
Les paramètres des procédures et des fonctions sont également transmis par l'intermédiaire de la pile qui reçoit une à une les valeurs correspondantes. La transmission se fait de gauche à droite, de sorte que le déplacement par rapport au pointeur de pile augmente également de gauche à droite.
 
Tout comme les variables locales, les paramètres transmis sont adressés par le registre Bi'. Mais contrairement aux variables locales, ils ont un déplacement positif. Examinez à cet effet le programme suivant:
 
Tout comme les variables locales, les paramètres transmis sont adressés par le registre Bi'. Mais contrairement aux variables locales, ils ont un déplacement positif. Examinez à cet effet le programme suivant:
 +
 
program p_para;J
 
program p_para;J
 
J
 
J
Ligne 498 : Ligne 533 :
 
cs:0023 E8DAFF cali TEST
 
cs:0023 E8DAFF cali TEST
 
J
 
J
end.
+
end.
 +
 
Dans cet exemple, deux paramètres de type Integer sont transmis à la procédure test. Le premier paramètre est transmis sous forme de valeur, et le second sous forme de variable, ce qui constitue une différence essentielle comme le montrent les commentaires du programme.
 
Dans cet exemple, deux paramètres de type Integer sont transmis à la procédure test. Le premier paramètre est transmis sous forme de valeur, et le second sous forme de variable, ce qui constitue une différence essentielle comme le montrent les commentaires du programme.
 +
 
Etudions d'abord l'appel de test à l'intérieur du programme principal. Le paramètre I est d'abord mis sur la pile avec la valeur 5. Concrètement, la valeur 5 est chargée dans AX et le registre AX est empilé. Ce mode de fonctionnement consiste à donner à test une copie locale du paramètre, une variable qui reflète sa valeur mais qui ne se confond pas avec lui. Les choses se passent différemment avec le deuxième paramètre pour lequel on mobilise la variable CI. Comme la procédure test doit traiter ce paramètre comme un paramètre variable, elle doit avoir la possibilité de le modifier durablement. Il ne suffit plus à Turbo Pascal de copier le contenu du paramètre sur la pile : il doit transmettre l'adresse où se trouve la variable pour que la procédure puisse véritablement y accéder.
 
Etudions d'abord l'appel de test à l'intérieur du programme principal. Le paramètre I est d'abord mis sur la pile avec la valeur 5. Concrètement, la valeur 5 est chargée dans AX et le registre AX est empilé. Ce mode de fonctionnement consiste à donner à test une copie locale du paramètre, une variable qui reflète sa valeur mais qui ne se confond pas avec lui. Les choses se passent différemment avec le deuxième paramètre pour lequel on mobilise la variable CI. Comme la procédure test doit traiter ce paramètre comme un paramètre variable, elle doit avoir la possibilité de le modifier durablement. Il ne suffit plus à Turbo Pascal de copier le contenu du paramètre sur la pile : il doit transmettre l'adresse où se trouve la variable pour que la procédure puisse véritablement y accéder.
2-56 La Bible Turbo Pascal
+
 
2-57 Dans les coulisses du langage
+
 
Même si les instructions Pascal ne le laissent pas voir directement, tous les accès effectués sur des paramètres variables à l'intérieur d'une procédure ou d'une fonction sont des opérations sur des pointeurs. Pour chaque paramètre variable, Turbo Pascal met en service un pointeur qui est -comme d'habitude dans ce langage- un pointeur de type FAR. La pile reçoit d'abord l'adresse du segment, et ensuite l'adresse du déplacement. Comme à chaque instruction PUSH le pointeur de pile se rapproche du début de segment de pile en mémoire centrale, l'adresse du déplacement précède finalement l'adresse du segment, exactement comme l'exige la convention Intel.
 
Même si les instructions Pascal ne le laissent pas voir directement, tous les accès effectués sur des paramètres variables à l'intérieur d'une procédure ou d'une fonction sont des opérations sur des pointeurs. Pour chaque paramètre variable, Turbo Pascal met en service un pointeur qui est -comme d'habitude dans ce langage- un pointeur de type FAR. La pile reçoit d'abord l'adresse du segment, et ensuite l'adresse du déplacement. Comme à chaque instruction PUSH le pointeur de pile se rapproche du début de segment de pile en mémoire centrale, l'adresse du déplacement précède finalement l'adresse du segment, exactement comme l'exige la convention Intel.
110V AX,5 110V DI,OFFSETGI CALLTEST PUSH BP
+
 
PUSH AX PUSH OS 110V SfSP
+
PUSH Dl
+
+
+
+
+
Ols
+
+
de retour [BP+21
+
+
+
$ SP
+
SS:0000 55:0000 SS:0000 SS:0000
+
Figure 18 Transmission de paramètres ô une procédure ou ô une fonction
+
 
La procédure Test initialise d'abord la pile de façon usuelle: elle peut alors accéder aux paramètres transmis au moyen du registre BP. Pour charger J avec la valeur de I, elle place d'abord le contenu de I dans AX. Pour pouvoir manipuler la variable provenant du paramètre J, elle doit en prendre l'adresse dans la pile. Cette opération est réalisée à l'aide de l'instruction LES que Turbo Pascal utilise toujours pour accéder aux variables référencées par des pointeurs. L'instruction en question charge un pointeur FAR situé à l'endroit indiqué, l'adresse du segment étant transférée dans ES et le déplacement dans le registre qui forme le premier opérande.
 
La procédure Test initialise d'abord la pile de façon usuelle: elle peut alors accéder aux paramètres transmis au moyen du registre BP. Pour charger J avec la valeur de I, elle place d'abord le contenu de I dans AX. Pour pouvoir manipuler la variable provenant du paramètre J, elle doit en prendre l'adresse dans la pile. Cette opération est réalisée à l'aide de l'instruction LES que Turbo Pascal utilise toujours pour accéder aux variables référencées par des pointeurs. L'instruction en question charge un pointeur FAR situé à l'endroit indiqué, l'adresse du segment étant transférée dans ES et le déplacement dans le registre qui forme le premier opérande.
 +
 
Ce registre doit obligatoirement être l'un des registres SI, DI ou BX.
 
Ce registre doit obligatoirement être l'un des registres SI, DI ou BX.
 +
 
Le processeur peut aussi charger le déplacement dans l'un des registres généraux, mais ceux-ci ne pouvant pas être utilisés pour l'adressage indexé ne sont pas qualifiés pour référencer des pointeurs.
 
Le processeur peut aussi charger le déplacement dans l'un des registres généraux, mais ceux-ci ne pouvant pas être utilisés pour l'adressage indexé ne sont pas qualifiés pour référencer des pointeurs.
Dans les coulisses du langage 2-57
+
 
La Bible Turbo Pascal 2-58
+
 
Turbo Pascal utilise généralement DI. Le registre de segment ne peut être que ES, car c'est ici le seul registre qui n'est pas astreint à pointer constamment sur le même segment. Dénommé "Extra Segment", il est justement réservé à ce type de tâche. Une fois l'adresse ainsi chargée, la procédure test est en mesure d'accéder à la variable transmise CI.
 
Turbo Pascal utilise généralement DI. Le registre de segment ne peut être que ES, car c'est ici le seul registre qui n'est pas astreint à pointer constamment sur le même segment. Dénommé "Extra Segment", il est justement réservé à ce type de tâche. Une fois l'adresse ainsi chargée, la procédure test est en mesure d'accéder à la variable transmise CI.
Figure 19: Clôture d'une procédure ou d'une foncilon et retraft des paramètres transmis
+
 
 
Quand la procédure est terminée, la pile est soumise à l'opération de nettoyage habituelle. Notez bien que l'instruction RET est utilisée ici sous une forme particulière, en association avec un nombre. Ce nombre représente le nombre d'octets ajoutés au contenu du pointeur de pile pour le ramener vers la fin de la pile. En même temps qu'elle communique l'adresse de retour, l'instruction RET ainsi exploitée permet d'éliminer les paramètres de la pile.
 
Quand la procédure est terminée, la pile est soumise à l'opération de nettoyage habituelle. Notez bien que l'instruction RET est utilisée ici sous une forme particulière, en association avec un nombre. Ce nombre représente le nombre d'octets ajoutés au contenu du pointeur de pile pour le ramener vers la fin de la pile. En même temps qu'elle communique l'adresse de retour, l'instruction RET ainsi exploitée permet d'éliminer les paramètres de la pile.
 +
 
L'exemple qui vient d'être donné montre comment Turbo Pascal transmet les paramètres par valeur lorsqu'ils ne sont pas destinés à être modifiés, et par adresse lorsqu'ils sont variables. Le type des paramètres n'influence pas leur transmission lorsqu'il s'agit de paramètres variables: dans ce cas la transmission s'effectue toujours sous forme de pointeur.
 
L'exemple qui vient d'être donné montre comment Turbo Pascal transmet les paramètres par valeur lorsqu'ils ne sont pas destinés à être modifiés, et par adresse lorsqu'ils sont variables. Le type des paramètres n'influence pas leur transmission lorsqu'il s'agit de paramètres variables: dans ce cas la transmission s'effectue toujours sous forme de pointeur.
 +
 
Mais il n'en est pas de même lorsque les paramètres sont transmis par valeur. La transmission dépend alors du type du paramètre, comme le montre l'exemple suivant:
 
Mais il n'en est pas de même lorsque les paramètres sont transmis par valeur. La transmission dépend alors du type du paramètre, comme le montre l'exemple suivant:
2-58 La Bible Turbo Pascal
+
 
2-59 Dans les coulisses du langage
+
 
program p_paral;J
 
program p_paral;J
 
'J
 
'J
Ligne 625 : Ligne 649 :
 
push dl U
 
push dl U
 
call TEST
 
call TEST
I)ans les coulisses du langage 2-59
+
 
La Bible Turbo Pascal 2-60
+
 
Pour les besoins de notre démonstration, nous avons transmis 6 différents paramètres à la procédure: un octet, un entier long, un pointeur, un réel, une chaîne de longueur 8 et une chaîne standard susceptible de contenir jusqu'à 255 caractères. Dans l'exemple précédent, le type entier Integer correspondait à une gestion interne par WORD. De même ici, le type Byte (octet) correspond à tous les types gérés de façon interne par BYTE. II s'agit non seulement des types Char, Shortlnt et Boolean mais aussi du type énuméré et du type intervalle qui sont susceptibles de tenir sur un octet.
 
Pour les besoins de notre démonstration, nous avons transmis 6 différents paramètres à la procédure: un octet, un entier long, un pointeur, un réel, une chaîne de longueur 8 et une chaîne standard susceptible de contenir jusqu'à 255 caractères. Dans l'exemple précédent, le type entier Integer correspondait à une gestion interne par WORD. De même ici, le type Byte (octet) correspond à tous les types gérés de façon interne par BYTE. II s'agit non seulement des types Char, Shortlnt et Boolean mais aussi du type énuméré et du type intervalle qui sont susceptibles de tenir sur un octet.
 
Les entiers longs et les pointeurs -qu'ils soient typés ou non- sont transmis de façon qu'ils se retrouvent dans la pile selon leur format normal. Avec les entiers longs (Longlnt) on empile d'abord le mot de poids fort puis le mot de poids faible. Avec les pointeurs on empile d'abord l'adresse du segment puis le déplacement. Les nombres en virgule flottante de type Real sont également passés sur la pile, avec préservation de leur format standard sur 6 octets. L'empilage s'effectue successivement sur le mot de poids fort, le mot de poids intermédiaire et le mot de poids faible.
 
Les entiers longs et les pointeurs -qu'ils soient typés ou non- sont transmis de façon qu'ils se retrouvent dans la pile selon leur format normal. Avec les entiers longs (Longlnt) on empile d'abord le mot de poids fort puis le mot de poids faible. Avec les pointeurs on empile d'abord l'adresse du segment puis le déplacement. Les nombres en virgule flottante de type Real sont également passés sur la pile, avec préservation de leur format standard sur 6 octets. L'empilage s'effectue successivement sur le mot de poids fort, le mot de poids intermédiaire et le mot de poids faible.
 +
 
La transmission des chaînes de caractères révèle une surprise de taille: le programme appelant transmet simplement un pointeur et la procédure commence par copier la chaîne dans une variable locale créée spécialement à cet effet, sansque le programmeur l'ait demandé. La place réservée est juste suffisante pour le but recherché :9 octets pour la variable S8, à partir de l'adresse [BP-$9] et 256 octets pour la variable S255, à partir de l'adresse [BP-$109].
 
La transmission des chaînes de caractères révèle une surprise de taille: le programme appelant transmet simplement un pointeur et la procédure commence par copier la chaîne dans une variable locale créée spécialement à cet effet, sansque le programmeur l'ait demandé. La place réservée est juste suffisante pour le but recherché :9 octets pour la variable S8, à partir de l'adresse [BP-$9] et 256 octets pour la variable S255, à partir de l'adresse [BP-$109].
La copie est prise en charge par une procédure interne de l'unité System appelée StrCopy, et que nous retrouverons souvent par la suite. Bien que son examen révèle qu'elle a été écrite en assembleur et non pas en Pascal, elle reçoit ses paramètres à la façon de Pascal au moyen de la pile. Ces paramètres sont au nombre de trois: le premier est un pointeur sur la chaîne source, le deuxième est aussi un pointeur qui pointe sur le tampon dédié à la copie locale, le troisième transmet la longueur maximale de la chaîne. Avant que la routine StrCopy ne duplique la chaîne source, elle teste sa longueur de façon à ne pas entreprendre de travail inutile et à ne copier que le strict nécessaire. Voici le listing de cette procédure en assembleur:
+
 
 +
La copie est prise en charge par une procédure interne de l'unité System appelée StrCopy, et que nous retrouverons souvent par la suite. Bien que son examen révèle qu'elle a été écrite en assembleur et non pas en Pascal, elle reçoit ses paramètres à la façon de Pascal au moyen de la pile. Ces paramètres sont au nombre de trois: le premier est un pointeur sur la chaîne source, le deuxième est aussi un pointeur qui pointe sur le tampon dédié à la copie locale, le troisième transmet la longueur maximale de la chaîne. Avant que la routine StrCopy ne duplique la chaîne source, elle teste sa longueur de façon à ne pas entreprendre de travail inutile et à ne copier que le strict nécessaire.  
 +
Voici le listing de cette procédure en assembleur:
 +
 
 
py proc far.J
 
py proc far.J
 
;Pour compter en incrémentation J
 
;Pour compter en incrémentation J
Ligne 653 : Ligne 680 :
 
ret 000A
 
ret 000A
 
St rC
 
St rC
2-60 La Bible Turbo Pascal
+
 
2-61 Dans les coulisses du langage
+
 
Nous venons d'examiner la transmission des paramètres standard. Mais comment se passe la transmission des types composés tels que tableaux, enregistrements ou ensembles? En fait, malgré leur diversité, Turbo Pascal traite ces types selon une règle unique que nous allons étudier à travers un exemple. Le programme suivant met en oeuvre 6 types de tableaux différents, ayant respectivement 1, 2, 3 4 5 et 6 octets. La mémoire occupée est donc en correspondance directe avec le nombre des éléments, et nous allons voir que c'est précisément cette quantité de mémoire qui va influencer le comportement de Turbo Pascal.
 
Nous venons d'examiner la transmission des paramètres standard. Mais comment se passe la transmission des types composés tels que tableaux, enregistrements ou ensembles? En fait, malgré leur diversité, Turbo Pascal traite ces types selon une règle unique que nous allons étudier à travers un exemple. Le programme suivant met en oeuvre 6 types de tableaux différents, ayant respectivement 1, 2, 3 4 5 et 6 octets. La mémoire occupée est donc en correspondance directe avec le nombre des éléments, et nous allons voir que c'est précisément cette quantité de mémoire qui va influencer le comportement de Turbo Pascal.
 +
 
program p_para2:.J
 
program p_para2:.J
 
.J
 
.J
Ligne 712 : Ligne 739 :
 
( cs0042 C21400 ret 0014 ;Enlève les paramètres de la )_i
 
( cs0042 C21400 ret 0014 ;Enlève les paramètres de la )_i
 
f ; pile Li
 
f ; pile Li
Dans les coulisses du langage 2-61
+
 
La Bible Turbo Pascal 2-62
+
 
begin.J
 
begin.J
 
test( gal. ga2. ga3, ga4. ga5. ga6 );.I
 
test( gal. ga2. ga3, ga4. ga5. ga6 );.I
Ligne 732 : Ligne 758 :
 
cs:006C E891FF cail TEST L-i
 
cs:006C E891FF cail TEST L-i
 
end.
 
end.
 +
 
La procédure test recueille six variables transmises par valeur, chacune d'elles correspondant à l'un des 6 types de tableaux. Les tableaux se présentent par ordre croissant de taille. Le premier paramètre est un tableau à un octet le deuxième un tableau à deux octets, et ainsi de suite. Notre attention va se focaliser sur la manière dont les paramètres sont mis sur la pile au moment où le programme principal appelle la procédure test.
 
La procédure test recueille six variables transmises par valeur, chacune d'elles correspondant à l'un des 6 types de tableaux. Les tableaux se présentent par ordre croissant de taille. Le premier paramètre est un tableau à un octet le deuxième un tableau à deux octets, et ainsi de suite. Notre attention va se focaliser sur la manière dont les paramètres sont mis sur la pile au moment où le programme principal appelle la procédure test.
 
Lepremier tableau qui ne comprend qu'un seul élément d'un octet est empilé comme un =format format interne WORD.
 
Lepremier tableau qui ne comprend qu'un seul élément d'un octet est empilé comme un =format format interne WORD.
Ligne 1 043 : Ligne 1 070 :
 
Pour terminer voici une récapitulation des différentes manières dont les fonctions retournent leurs résultats en fonction de leur type:
 
Pour terminer voici une récapitulation des différentes manières dont les fonctions retournent leurs résultats en fonction de leur type:
 
Registres ulilisôs pour retourner les résultats des fonctions
 
Registres ulilisôs pour retourner les résultats des fonctions
2-68 La Bible Turbo Pascal
+
 
2-69 Dans les coulisses du langage
+
===Récursion===
V Récursion
+
 
Les fonctions récursives ont longtemps été, pour de nombreuses générations de programmeurs, le dernier mystère à percer: je suis moi-même passé par là. Faute de pouvoir être appréhendé de façon rationnelle, ce domaine a souvent conservé une aura mystique, qu'on ne retrouve plus par ailleurs dans la société informatisée, sauf peut-être dans les jeux d'aventure. Il  a des choses qui fonctionnent sans qu'on sache exactement comment, mais après tout du moment que ça marche, que souhaiter de plus?
 
Les fonctions récursives ont longtemps été, pour de nombreuses générations de programmeurs, le dernier mystère à percer: je suis moi-même passé par là. Faute de pouvoir être appréhendé de façon rationnelle, ce domaine a souvent conservé une aura mystique, qu'on ne retrouve plus par ailleurs dans la société informatisée, sauf peut-être dans les jeux d'aventure. Il  a des choses qui fonctionnent sans qu'on sache exactement comment, mais après tout du moment que ça marche, que souhaiter de plus?
 +
 
En fait les calculs récursifs ne font pas appel à des ressources particulières du processeur, leur maniement nécessite simplement un peu plus de matière grise. Pour le processeur, il n'y a là rien de spécial. Les appels de fonctions et de procédures récursives sont pour lui du domaine de l'ordinaire, il ne s'occupe pas du fait qu'une procédure s'appelle elle-même et engage une récursion plutôt que d'appeler une autre procédure.
 
En fait les calculs récursifs ne font pas appel à des ressources particulières du processeur, leur maniement nécessite simplement un peu plus de matière grise. Pour le processeur, il n'y a là rien de spécial. Les appels de fonctions et de procédures récursives sont pour lui du domaine de l'ordinaire, il ne s'occupe pas du fait qu'une procédure s'appelle elle-même et engage une récursion plutôt que d'appeler une autre procédure.
 +
 
Les chapitres précédents consacrés à la gestion des variables locales et la transmission des paramètres ont révélé indirectement la raison de ce comportement. Il ne s'agit plus pour nous que de rassembler les pièces du puzzle.
 
Les chapitres précédents consacrés à la gestion des variables locales et la transmission des paramètres ont révélé indirectement la raison de ce comportement. Il ne s'agit plus pour nous que de rassembler les pièces du puzzle.
 +
 
Pour cela nous allons nous aider d'une fonction récursive très simple : la fonction factorielle. Cette fonction, à vrai dire, serait plus efficace si nous la programmions avec le schéma itératif classique. Mais comme elle fonctionne aussi sous forme de récursion simple et brève, nous allons l'étudier en détail.
 
Pour cela nous allons nous aider d'une fonction récursive très simple : la fonction factorielle. Cette fonction, à vrai dire, serait plus efficace si nous la programmions avec le schéma itératif classique. Mais comme elle fonctionne aussi sous forme de récursion simple et brève, nous allons l'étudier en détail.
 +
 
La factorielle est issue de la théorie des probabilités et sert à trouver la réponse à des problèmes du genre:
 
La factorielle est issue de la théorie des probabilités et sert à trouver la réponse à des problèmes du genre:
 +
 
"Combien d'arrangements peut-on réaliser avec trois cartes de couleur différente (rouge, verte et bleue) disposées l'une à côté de l'autre ?"
 
"Combien d'arrangements peut-on réaliser avec trois cartes de couleur différente (rouge, verte et bleue) disposées l'une à côté de l'autre ?"
 
La solution est facile à construire. Pour la carte de gauche, il existe 3 possibilités, car chacune des 3 cartes convient. Pour la carte du milieu, il n'existe plus que 2 possibilités, car l'une des cartes est déjà utilisée. Pour la troisième carte, il ne reste plus qu'une possibilité correspondant à la dernière carte disponible. Le nombre total de possibilités est donc 3*2*1 = 6. C'est ce type de calcul que réalise la factorielle FAC, définie récursivement de la façon suivante:
 
La solution est facile à construire. Pour la carte de gauche, il existe 3 possibilités, car chacune des 3 cartes convient. Pour la carte du milieu, il n'existe plus que 2 possibilités, car l'une des cartes est déjà utilisée. Pour la troisième carte, il ne reste plus qu'une possibilité correspondant à la dernière carte disponible. Le nombre total de possibilités est donc 3*2*1 = 6. C'est ce type de calcul que réalise la factorielle FAC, définie récursivement de la façon suivante:
Ligne 1 215 : Ligne 1 246 :
 
2-72 La Bible Turbo Pascal
 
2-72 La Bible Turbo Pascal
 
2-73 Dans les coulisses du langage
 
2-73 Dans les coulisses du langage
2.5. Pointeurs
+
==Pointeurs==
 
Un nouveau mode de pensée ........................................73 Les pointeurs dans la programmation système ........................79 W1TH et les pointeurs ..............................................83 Arithmétique des pointeurs .........................................87 Les pointeurs de code ..............................................89
 
Un nouveau mode de pensée ........................................73 Les pointeurs dans la programmation système ........................79 W1TH et les pointeurs ..............................................83 Arithmétique des pointeurs .........................................87 Les pointeurs de code ..............................................89
 
Les pointeurs jouent un rôle important dans la programmation en Pascal. Qu'ils servent à réaliser des listes chaînées ou des arbres binaires, ce sont toujours des pointeurs qui permettent de relier entre eux des éléments appartenant à des structures de données complexes.
 
Les pointeurs jouent un rôle important dans la programmation en Pascal. Qu'ils servent à réaliser des listes chaînées ou des arbres binaires, ce sont toujours des pointeurs qui permettent de relier entre eux des éléments appartenant à des structures de données complexes.
 
Les pointeurs sont également importants dans la programmation système, notamment lorsqu'on travaille avec des fonctions du DOS ou du BIOS. Chaque fois que les fonctions appelées manipulent des tampons et des structures de données, ce sont des pointeurs qui sont mis en service pour les référencer.
 
Les pointeurs sont également importants dans la programmation système, notamment lorsqu'on travaille avec des fonctions du DOS ou du BIOS. Chaque fois que les fonctions appelées manipulent des tampons et des structures de données, ce sont des pointeurs qui sont mis en service pour les référencer.
 
Il y a donc bien des raisons d'étudier la structure, l'utilisation et les possibilités des pointeurs en Turbo Pascal.
 
Il y a donc bien des raisons d'étudier la structure, l'utilisation et les possibilités des pointeurs en Turbo Pascal.
V Un nouveau mode de pensée
+
===Un nouveau mode de pensée===
 
Bien que les pointeurs soient une notion importante en Pascal, ils offrent quelques difficultés aux programmeurs, notamment aux débutants. Pour comprendre leur fonctionnement il faut en général réformer un peu sa manière de penser. Il peut s'écouler un temps assez long jusqu'à ce que la lumière se fasse et ramène les pointeurs au rang d'objets tout à fait ordinaires dela programmation.
 
Bien que les pointeurs soient une notion importante en Pascal, ils offrent quelques difficultés aux programmeurs, notamment aux débutants. Pour comprendre leur fonctionnement il faut en général réformer un peu sa manière de penser. Il peut s'écouler un temps assez long jusqu'à ce que la lumière se fasse et ramène les pointeurs au rang d'objets tout à fait ordinaires dela programmation.
 
Il faut bien se rendre compte que les pointeurs n'ont pas un contenu semblable à celui des variables de type Char, Integer, Boolean mais qu'ils contiennent une valeur interprétée par Turbo Pascal comme un numéro de mémoire (vive ou morte). Le type de la variable qui se trouve à l'adresse indiquée doit être précisée au moment de la définition du pointeur. Ainsi:
 
Il faut bien se rendre compte que les pointeurs n'ont pas un contenu semblable à celui des variables de type Char, Integer, Boolean mais qu'ils contiennent une valeur interprétée par Turbo Pascal comme un numéro de mémoire (vive ou morte). Le type de la variable qui se trouve à l'adresse indiquée doit être précisée au moment de la définition du pointeur. Ainsi:
Ligne 1 363 : Ligne 1 394 :
 
Move( p2A, p1, 100 ); C Instruction correcte )J
 
Move( p2A, p1, 100 ); C Instruction correcte )J
 
end.
 
end.
2-78 La Bible Turbo Pascal
+
 
2-79 Dans les coulisses du langage
+
===Les pointeurs dans la programmation système===
V Les pointeurs dans la programmation système
+
 
Les pointeurs jouent un grand rôle dans la programmation système, car beaucoup de fonctions du DOS et du BIOS utilisent des pointeurs dès qu'interviennent des tampons et des structures de données. Que ces fonctions attendent des pointeurs comme arguments d'entrée ou retournent des pointeurs comme résultats, il faut connaître leur fonctionnement, leur structure et les possibilités de transtypage pour maîtriser le dialogue entre le programme et le système de base. La présente section se propose de vous présenter un exemple pratique de programmation système pour vous familiariser avec les problèmes qu'elle pose.
 
Les pointeurs jouent un grand rôle dans la programmation système, car beaucoup de fonctions du DOS et du BIOS utilisent des pointeurs dès qu'interviennent des tampons et des structures de données. Que ces fonctions attendent des pointeurs comme arguments d'entrée ou retournent des pointeurs comme résultats, il faut connaître leur fonctionnement, leur structure et les possibilités de transtypage pour maîtriser le dialogue entre le programme et le système de base. La présente section se propose de vous présenter un exemple pratique de programmation système pour vous familiariser avec les problèmes qu'elle pose.
 
Nous nous intéresserons plus spécialement à un algorithme capable de nous indiquer les désignations alphabétiques des lecteurs installés sous DOS. Bien que de nombreuses applications aient besoin de ce genre de routine, par exemple pour permettre à l'utilisateur de sélectionner un lecteur, le système DOS ne proposait jusqu'à la version 3.0 aucune fonction capable de donner cette information. Il fallait alors se rabattre sur des structures de DOS non documentées et par conséquent secrètes.
 
Nous nous intéresserons plus spécialement à un algorithme capable de nous indiquer les désignations alphabétiques des lecteurs installés sous DOS. Bien que de nombreuses applications aient besoin de ce genre de routine, par exemple pour permettre à l'utilisateur de sélectionner un lecteur, le système DOS ne proposait jusqu'à la version 3.0 aucune fonction capable de donner cette information. Il fallait alors se rabattre sur des structures de DOS non documentées et par conséquent secrètes.
Ligne 1 567 : Ligne 1 597 :
 
Au centre de ce programme se trouvent les variables globales Unelnfo et Tablolnfo, qui dépendent de l'enregistrement Info. La variable Unelnfo représente un exemplaire unique de l'enregistrement, tandis que Tablolnfo est constitué par un tableau de dix enregistrements de ce type.
 
Au centre de ce programme se trouvent les variables globales Unelnfo et Tablolnfo, qui dépendent de l'enregistrement Info. La variable Unelnfo représente un exemplaire unique de l'enregistrement, tandis que Tablolnfo est constitué par un tableau de dix enregistrements de ce type.
 
L'instruction WITH n'est pas compilée de la même façon dans les deux cas. Dans un premier temps, on accède d'abord, par le moyen d'un WITH, à la variable Unelnfo.
 
L'instruction WITH n'est pas compilée de la même façon dans les deux cas. Dans un premier temps, on accède d'abord, par le moyen d'un WITH, à la variable Unelnfo.
2-84 La Bible Turbo Pascal
+
 
2-85 Dans les coulisses du langage
+
 
L'adresse de cette variable est complètement déterminée au moment de la compilation. De ce fait, Turbo Pascal n'a pas besoin de pointeur pour accéder aux champs de l'enregistrement : il peut les adresser directement à l'intérieur du segment DS. On voit que l'instruction WITH n'est même pas véritablement traduite en code machine, elle sert uniquement à simplifier l'écriture des ordres Pascal.
 
L'adresse de cette variable est complètement déterminée au moment de la compilation. De ce fait, Turbo Pascal n'a pas besoin de pointeur pour accéder aux champs de l'enregistrement : il peut les adresser directement à l'intérieur du segment DS. On voit que l'instruction WITH n'est même pas véritablement traduite en code machine, elle sert uniquement à simplifier l'écriture des ordres Pascal.
 +
 
Les choses se passent différemment avec le tableau de données. Ses différents éléments sont parcourus par une boucle FOR NEXT et leurs champs sont adressés au moyen d'une instruction WITH. Mais ici Turbo Pascal ne connaît pas encore l'adresse des enregistrements au moment de la compilation, car l'indice I ne prend une valeur concrète qu'au moment de l'exécution de la boucle FOR NEXT. Dans cette situation Turbo Pascal fait appel à une variable locale dans laquelle il reporte l'adresse de l'enregistrement au passage de l'instruction WITH. Comme les éléments d'un tableau sont disposés l'un derrière l'autre d'une façon strictement contiguë, leur adresse s'obtient en multipliant leur indice par la longueur d'un élément du type Info (3 octets).
 
Les choses se passent différemment avec le tableau de données. Ses différents éléments sont parcourus par une boucle FOR NEXT et leurs champs sont adressés au moyen d'une instruction WITH. Mais ici Turbo Pascal ne connaît pas encore l'adresse des enregistrements au moment de la compilation, car l'indice I ne prend une valeur concrète qu'au moment de l'exécution de la boucle FOR NEXT. Dans cette situation Turbo Pascal fait appel à une variable locale dans laquelle il reporte l'adresse de l'enregistrement au passage de l'instruction WITH. Comme les éléments d'un tableau sont disposés l'un derrière l'autre d'une façon strictement contiguë, leur adresse s'obtient en multipliant leur indice par la longueur d'un élément du type Info (3 octets).
 +
 
Ce faisant, Turbo Pascal ne recueille que l'adresse du déplacement de l'enregistrement par rapport au début du tableau en mémoire. Il faut encore ajouter l'adresse de base du tableau.
 
Ce faisant, Turbo Pascal ne recueille que l'adresse du déplacement de l'enregistrement par rapport au début du tableau en mémoire. Il faut encore ajouter l'adresse de base du tableau.
 
Notez bien que dans cette opération Turbo Pascal n'additionne pas exactement l'adresse de base, mais l'adresse de base diminuée de trois. Cette anomalie apparente est due au mode d'indexation et de mémorisation des tableaux. Lors de la déclaration de Tablolnfo, les indices affectés aux éléments ont été définis de I à 10. Le premier élément d'indice 1 se trouve à l'offset O par rapport au début du tableau, et non à l'offset 3, car on n'a pas défini d'élément d'indice 0. Turbo Pascal devrait donc décrémenter l'indice de I avant de le multiplier par la longueur de l'élément. Mais ceci nécessiterait une instruction machine supplémentaire. Il est facile de l'éviter en retranchant les octets excédentaires à la fin du calcul, au moment de l'addition de l'adresse de base. Le principe arithmétique appliqué est le suivant:
 
Notez bien que dans cette opération Turbo Pascal n'additionne pas exactement l'adresse de base, mais l'adresse de base diminuée de trois. Cette anomalie apparente est due au mode d'indexation et de mémorisation des tableaux. Lors de la déclaration de Tablolnfo, les indices affectés aux éléments ont été définis de I à 10. Le premier élément d'indice 1 se trouve à l'offset O par rapport au début du tableau, et non à l'offset 3, car on n'a pas défini d'élément d'indice 0. Turbo Pascal devrait donc décrémenter l'indice de I avant de le multiplier par la longueur de l'élément. Mais ceci nécessiterait une instruction machine supplémentaire. Il est facile de l'éviter en retranchant les octets excédentaires à la fin du calcul, au moment de l'addition de l'adresse de base. Le principe arithmétique appliqué est le suivant:
Ligne 1 876 : Ligne 1 907 :
 
Regardez bien comment, à l'intérieur de la procédure CaliThem, Turbo Pascal appelle les procédures et fonctions par leurs pointeurs. Pour ce qui est de la transmission des paramètres et du retour du résultat, le mécanisme est usuel. Seule l'instruction CALL qui appelle les sous-programmes est inhabituelle, car l'adresse cible n'est pas une constante. Elle est issue d'un pointeur de code qui fait partie de l'enregistrement transmis sous forme de paramètre variable (c'est-à-dire de pointeur de donnée). Ainsi Turbo Pascal charge le pointeur qui pointe sur l'enregistrement dans les registres ES:DI. Les pointeurs de code peuvent donc être atteints par ES:[DI], ES:[DI+4], etc.... Pour exécuter l'instruction CALL, le processeur va chercher l'adresse FAR du sous-programme à appeler dans la mémoire ES:[DI+xx] correspondante puis il poursuit le déroulement du programme à cette adresse.
 
Regardez bien comment, à l'intérieur de la procédure CaliThem, Turbo Pascal appelle les procédures et fonctions par leurs pointeurs. Pour ce qui est de la transmission des paramètres et du retour du résultat, le mécanisme est usuel. Seule l'instruction CALL qui appelle les sous-programmes est inhabituelle, car l'adresse cible n'est pas une constante. Elle est issue d'un pointeur de code qui fait partie de l'enregistrement transmis sous forme de paramètre variable (c'est-à-dire de pointeur de donnée). Ainsi Turbo Pascal charge le pointeur qui pointe sur l'enregistrement dans les registres ES:DI. Les pointeurs de code peuvent donc être atteints par ES:[DI], ES:[DI+4], etc.... Pour exécuter l'instruction CALL, le processeur va chercher l'adresse FAR du sous-programme à appeler dans la mémoire ES:[DI+xx] correspondante puis il poursuit le déroulement du programme à cette adresse.
 
En guise de conclusion, on retiendra que les pointeurs de code sont avantageux chaque fois qu'une procédure ou une fonction est construite comme un cadre général, une structure universelle à l'intérieur de laquelle les pointeurs effectuent les adaptations particulières. C'est dans ce sens que nous rencontrerons encore souvent les pointeurs de code dans la suite de cet ouvrage.
 
En guise de conclusion, on retiendra que les pointeurs de code sont avantageux chaque fois qu'une procédure ou une fonction est construite comme un cadre général, une structure universelle à l'intérieur de laquelle les pointeurs effectuent les adaptations particulières. C'est dans ce sens que nous rencontrerons encore souvent les pointeurs de code dans la suite de cet ouvrage.
2-94 La Bible Turbo Pascal
+
 
2-95 Dans les coulisses du langage
+
==Gestion du tas==
2.6. Gestion du tas
+
 
GetMem et Freemem ...............................................95 Mark et Release ..................................................102 Une nouvelle routine d'erreur pour le tas ...........................103
 
GetMem et Freemem ...............................................95 Mark et Release ..................................................102 Une nouvelle routine d'erreur pour le tas ...........................103
 
Le tas (en anglais heap) est une zone de la mémoire qui joue un grand rôle un Pascal. Alors que dans le segment de données les variables sont mémorisées de façon purement statique, le tas est fait pour recevoir des allocations dynamiques de variables et de tampons. Comme cette allocation n'a lieu qu'au moment de l'exécution, elle est très précieuse pour manier les structures dont la taille dépend des données et n'est connue qu'à l'exécution du programme. Réaliser des arbres, des listes chaînées ou des graphes sans l'appui du tas relève de la gageure. Cette section est destinée à vous montrer quelques-uns des mécanismes qui commandent la gestion de cet espace mémoire.
 
Le tas (en anglais heap) est une zone de la mémoire qui joue un grand rôle un Pascal. Alors que dans le segment de données les variables sont mémorisées de façon purement statique, le tas est fait pour recevoir des allocations dynamiques de variables et de tampons. Comme cette allocation n'a lieu qu'au moment de l'exécution, elle est très précieuse pour manier les structures dont la taille dépend des données et n'est connue qu'à l'exécution du programme. Réaliser des arbres, des listes chaînées ou des graphes sans l'appui du tas relève de la gageure. Cette section est destinée à vous montrer quelques-uns des mécanismes qui commandent la gestion de cet espace mémoire.
 
Par défaut, Turbo Pascal réservepour le tas toute la mémoire comprise entre la fin du segment de données et la fin de la mémoire vive (jusqu'à la frontière des 640 Ko). Si cette quantité est excessive pour vos besoins, vous pouvez la changer au moyen de l'option "Compiler/Memory Size" du menu "Options" ou en installant la directive de compilation ($M). De la même manière vous pouvez aussi fixer une taille minimale pour le tas si au moment de la compilation vous êtes en mesure de la prévoir. Au niveau du système d'exploitation, le loader EXEC ne chargera le programme que si vos exigences peuvent être satisfaites. Si la mémoire est insuffisante, le démarrage du programme sera interrompu avec émission d'un message d'erreur approprié.
 
Par défaut, Turbo Pascal réservepour le tas toute la mémoire comprise entre la fin du segment de données et la fin de la mémoire vive (jusqu'à la frontière des 640 Ko). Si cette quantité est excessive pour vos besoins, vous pouvez la changer au moyen de l'option "Compiler/Memory Size" du menu "Options" ou en installant la directive de compilation ($M). De la même manière vous pouvez aussi fixer une taille minimale pour le tas si au moment de la compilation vous êtes en mesure de la prévoir. Au niveau du système d'exploitation, le loader EXEC ne chargera le programme que si vos exigences peuvent être satisfaites. Si la mémoire est insuffisante, le démarrage du programme sera interrompu avec émission d'un message d'erreur approprié.
 
Pour gérer le tas au niveau interne, Turbo Pascal se sert de trois pointeurs appelés HeapOrg, HeapPtr et FreePtr et qui sont déclarés au sein de l'unité System. C'est à cet endroit qu'ils sont initialisés avant même le début du programme. Tout au long de l'exécution du programme, HeapOrg pointe sur le début du tas. Le contenu de HeapPtr est plus changeant, car il pointe sur la fin du tas alloué et suit donc les mouvements d'allocation et de désallocation des blocs mémoire. Le pointeur FreePtr a lui aussi la bougeotte car il référence la liste des fragments dont nous allons parler par la suite.
 
Pour gérer le tas au niveau interne, Turbo Pascal se sert de trois pointeurs appelés HeapOrg, HeapPtr et FreePtr et qui sont déclarés au sein de l'unité System. C'est à cet endroit qu'ils sont initialisés avant même le début du programme. Tout au long de l'exécution du programme, HeapOrg pointe sur le début du tas. Le contenu de HeapPtr est plus changeant, car il pointe sur la fin du tas alloué et suit donc les mouvements d'allocation et de désallocation des blocs mémoire. Le pointeur FreePtr a lui aussi la bougeotte car il référence la liste des fragments dont nous allons parler par la suite.
 +
 
Cet attelage à trois chevaux est commandé par les différentes procédures et fonctions que Turbo Pascal propose pour accéder au tas. Ces fonctions peuvent être réparties en deux groupes. D'un côté on peut ranger les ordres Mark et Release, qui permettent d'allouer et de libérer la mémoire d'une façon simple et brute. De l'autre côté on trouve les procédures GetMem et FreeMem, ainsi que New et Dispose. Comme le montrera le chapitre 2.7, New et Dispose font eux-mêmes appel à GetMem et FreeMem. Le cercle des candidats se réduit donc aux couples Mark et Release d'une part, et GetMem et FreeMem d'autre part. Nous allons d'abord nous intéresser au fonctionnement de ces deux dernières procédures.
 
Cet attelage à trois chevaux est commandé par les différentes procédures et fonctions que Turbo Pascal propose pour accéder au tas. Ces fonctions peuvent être réparties en deux groupes. D'un côté on peut ranger les ordres Mark et Release, qui permettent d'allouer et de libérer la mémoire d'une façon simple et brute. De l'autre côté on trouve les procédures GetMem et FreeMem, ainsi que New et Dispose. Comme le montrera le chapitre 2.7, New et Dispose font eux-mêmes appel à GetMem et FreeMem. Le cercle des candidats se réduit donc aux couples Mark et Release d'une part, et GetMem et FreeMem d'autre part. Nous allons d'abord nous intéresser au fonctionnement de ces deux dernières procédures.
V GetMem et FreeMem
+
===GetMem et FreeMem===
 
Examinons ce qui se passe au premier appel de GetMem après le lancement du programme. GetMem doit d'abord vérifier s'il reste encore assez de place entre la fin du tas alloué et la fin de l'espace global attribué au tas. En général, il restera de la place
 
Examinons ce qui se passe au premier appel de GetMem après le lancement du programme. GetMem doit d'abord vérifier s'il reste encore assez de place entre la fin du tas alloué et la fin de l'espace global attribué au tas. En général, il restera de la place
 
Dans les coulisses du langage 2-95
 
Dans les coulisses du langage 2-95
Ligne 2 127 : Ligne 2 158 :
 
Cl rScr;.J
 
Cl rScr;.J
 
end.
 
end.
V Mark et Release
+
===Mark et Release===
 
Les procédures Mark et Release gèrent le tas d'une manière sensiblement moins sophistiquée. Elles influencent directement le pointeur HeapPtr. La tâche de Mark consiste à renvoyer le contenu courant de HeapPtr au programme appelant qui apprend ainsi où se trouve la fin du tas alloué. Ce pointeur, ou un autre, pourra ensuite être transmis à Release. Cette dernière procédure charge le pointeur transmis dans HeapPtr et efface en même temps la liste des fragments, de sorte que tous les trous disparaissent (on pourrait dire: sont "raccommodés").
 
Les procédures Mark et Release gèrent le tas d'une manière sensiblement moins sophistiquée. Elles influencent directement le pointeur HeapPtr. La tâche de Mark consiste à renvoyer le contenu courant de HeapPtr au programme appelant qui apprend ainsi où se trouve la fin du tas alloué. Ce pointeur, ou un autre, pourra ensuite être transmis à Release. Cette dernière procédure charge le pointeur transmis dans HeapPtr et efface en même temps la liste des fragments, de sorte que tous les trous disparaissent (on pourrait dire: sont "raccommodés").
 
L'utilisation de Mark et Release en conjonction avec GetMem et FreeMem peut avoir des conséquences désastreuses. Elle n'est recommandée que lorsqu'un espace mémoire important a été alloué par GetMem et que l'on est sûr que le tas n'était pas fragmenté au préalable, de sorte que les blocs alloués sont contigus. On peut alors éviter de libérer l'espace alloué bloc par bloc au moyen de FreeMem et adopter la tactique suivante:
 
L'utilisation de Mark et Release en conjonction avec GetMem et FreeMem peut avoir des conséquences désastreuses. Elle n'est recommandée que lorsqu'un espace mémoire important a été alloué par GetMem et que l'on est sûr que le tas n'était pas fragmenté au préalable, de sorte que les blocs alloués sont contigus. On peut alors éviter de libérer l'espace alloué bloc par bloc au moyen de FreeMem et adopter la tactique suivante:
Ligne 2 167 : Ligne 2 198 :
 
Iin yHeapError 1; end;
 
Iin yHeapError 1; end;
 
Figure 27: Appel dune routine d'erreur par HeapError
 
Figure 27: Appel dune routine d'erreur par HeapError
2.7. L'unité System
+
 
 +
==L'unité System==
 
Abs.............................................................105 Addr, opérateur ©.................................................106 Chr.............................................................108 CSeg, DSeg, SSeg, SPtr ............................................109
 
Abs.............................................................105 Addr, opérateur ©.................................................106 Chr.............................................................108 CSeg, DSeg, SSeg, SPtr ............................................109
 
Dec, Inc .........................................................109
 
Dec, Inc .........................................................109
Ligne 2 174 : Ligne 2 206 :
 
2-104 La Bible Turbo Pascal
 
2-104 La Bible Turbo Pascal
 
2-105 Dans les coulisses du langage
 
2-105 Dans les coulisses du langage
 +
 
L'unité System n'est pas simplement une unité parmi d'autres, elle présente des caractères particuliers. En témoigne d'ailleurs son inclusion automatique sans instruction USES explicite. Mais il existe d'autres raisons qui font que l'unité System est pour Turbo Pascal plus qu'une collection de procédures et de fonctions librement appelables.
 
L'unité System n'est pas simplement une unité parmi d'autres, elle présente des caractères particuliers. En témoigne d'ailleurs son inclusion automatique sans instruction USES explicite. Mais il existe d'autres raisons qui font que l'unité System est pour Turbo Pascal plus qu'une collection de procédures et de fonctions librement appelables.
 +
 
II semble même que cette unité ait une part d'intelligence propre qui lui permet par exemple d'appliquer une même fonction à des types de données tout à fait différents ou de générer du code spécifique selon le type de données traité. Même en version 5.0 (et 4.0) Turbo Pascal est déjà un langage orienté objet en ce sens qu'il tient compte au moment de la compilation du type d'argument transmis à une fonction ou à une procédure.
 
II semble même que cette unité ait une part d'intelligence propre qui lui permet par exemple d'appliquer une même fonction à des types de données tout à fait différents ou de générer du code spécifique selon le type de données traité. Même en version 5.0 (et 4.0) Turbo Pascal est déjà un langage orienté objet en ce sens qu'il tient compte au moment de la compilation du type d'argument transmis à une fonction ou à une procédure.
 +
 
Il est également intéressant de remarquer que de nombreuses fonctions ont été conçues en assembleur et qu'elles ne sont pas appelées sous forme de sous-programmes. Turbo Pascal traduit les appels correspondants en séquence d'ordres machine, œ qui rappelle les procédures Inline qui insèrent directement du code assembleur. Ces séquences dépendent du type et même de la valeur des paramètres transmis, dans la mesure où ils sont connus au moment de la compilation. Il s'avère finalement que l'unité System est plus qu'une unité au sens propre du terme, elle est une composante fixe du compilateur. Comment expliquer autrement que des mécanismes d'optimisation y aient été implémentés alors qu'ils font défaut aux autres unités?
 
Il est également intéressant de remarquer que de nombreuses fonctions ont été conçues en assembleur et qu'elles ne sont pas appelées sous forme de sous-programmes. Turbo Pascal traduit les appels correspondants en séquence d'ordres machine, œ qui rappelle les procédures Inline qui insèrent directement du code assembleur. Ces séquences dépendent du type et même de la valeur des paramètres transmis, dans la mesure où ils sont connus au moment de la compilation. Il s'avère finalement que l'unité System est plus qu'une unité au sens propre du terme, elle est une composante fixe du compilateur. Comment expliquer autrement que des mécanismes d'optimisation y aient été implémentés alors qu'ils font défaut aux autres unités?
 +
 
Le tableau suivant liste les fonctions, procédures et références de variables de l'unité System que Turbo Pascal à chaque appel transforme directement en séquences d'assembleur:
 
Le tableau suivant liste les fonctions, procédures et références de variables de l'unité System que Turbo Pascal à chaque appel transforme directement en séquences d'assembleur:
 
Abs Dseg Length Pred SSeg
 
Abs Dseg Length Pred SSeg
Ligne 2 183 : Ligne 2 219 :
 
CSeg Inc Ord SizeOf
 
CSeg Inc Ord SizeOf
 
Dec Lo PI SPtr
 
Dec Lo PI SPtr
 +
 
Dans les pages qui suivent je voudrais vous présenter les différentes fonctions et leurs séquences de transformation. En règle générale, chaque fonction sera décrite par plusieurs appels mettant en jeu divers paramètres et diverses affectations du résultat. Si les arguments de la fonction sont des variables, ces dernières ne seront pas initialisées par le programme, de sorte que l'appel de la fonction à l'intérieur d'un programme réel est dénué de sens. Mais comme il s'agit ici de programmes de démonstration, nous ne nous arrêterons pas sur ce problème.
 
Dans les pages qui suivent je voudrais vous présenter les différentes fonctions et leurs séquences de transformation. En règle générale, chaque fonction sera décrite par plusieurs appels mettant en jeu divers paramètres et diverses affectations du résultat. Si les arguments de la fonction sont des variables, ces dernières ne seront pas initialisées par le programme, de sorte que l'appel de la fonction à l'intérieur d'un programme réel est dénué de sens. Mais comme il s'agit ici de programmes de démonstration, nous ne nous arrêterons pas sur ce problème.
vAbs
+
 
 +
===Abs===
 
La fonction Abs renvoie la valeur absolue (= positive) de l'argument transmis, qui doit nécessairement appartenir à la classe des entiers (Byte, Shortint, Word, Longlnt) ou des réels. Si Turbo Pascal peut connaître la valeur de l'expression au moment de la compilation, il s'agit alors d'une constante au sens général du terme. Sa valeur absolue est déterminée à la compilation et affectée à la variable appropriée par un
 
La fonction Abs renvoie la valeur absolue (= positive) de l'argument transmis, qui doit nécessairement appartenir à la classe des entiers (Byte, Shortint, Word, Longlnt) ou des réels. Si Turbo Pascal peut connaître la valeur de l'expression au moment de la compilation, il s'agit alors d'une constante au sens général du terme. Sa valeur absolue est déterminée à la compilation et affectée à la variable appropriée par un
Dans les coulisses du langage 2-105
 
La Bible Turbo Pascal 2-106
 
 
ordre MOV. A cette occasion, Turbo Pascal est capable de déceler le franchissement éventuel de l'intervalle de définition. Le programme qui suit essaye d'attribuer la valeur absolue de 300 à une variable de type Byte. Turbo Pascal réagit en interrompant la compilation et en affichant un message d'erreur.
 
ordre MOV. A cette occasion, Turbo Pascal est capable de déceler le franchissement éventuel de l'intervalle de définition. Le programme qui suit essaye d'attribuer la valeur absolue de 300 à une variable de type Byte. Turbo Pascal réagit en interrompant la compilation et en affichant un message d'erreur.
 +
 
Si l'expression ne peut être évaluée qu'au moment de l'exécution du programme, Turbo Pascal génère les instructions capables de la calculer et teste ensuite si le résultat est positif ou négatif. Dans ce dernier cas, le résultat est transformé en son opposé par l'ordre NEG de façon à devenir positif. La manipulation correspondante ne peut être effectuée directement que si l'expression est entière. Pour établir la valeur absolue d'un résultat en virgule flottante, le compilateur fait appel à un sous-programme car le calcul est trop complexe et trop long pour être incorporé sous forme de code machine.
 
Si l'expression ne peut être évaluée qu'au moment de l'exécution du programme, Turbo Pascal génère les instructions capables de la calculer et teste ensuite si le résultat est positif ou négatif. Dans ce dernier cas, le résultat est transformé en son opposé par l'ordre NEG de façon à devenir positif. La manipulation correspondante ne peut être effectuée directement que si l'expression est entière. Pour établir la valeur absolue d'un résultat en virgule flottante, le compilateur fait appel à un sous-programme car le calcul est trop complexe et trop long pour être incorporé sous forme de code machine.
 
C—ABS: à étudier le code machine
 
C—ABS: à étudier le code machine
Ligne 2 234 : Ligne 2 271 :
 
(Sendi f)J
 
(Sendi f)J
 
J
 
J
V Addr, opérateur @
+
===Addr, opérateur @===
 
Comme l'opérateur @, la fonction Addr sert à connaître l'adresse d'une variable, d'une constante typée ou d'une procédure/ fonction. Indépendamment du type de
 
Comme l'opérateur @, la fonction Addr sert à connaître l'adresse d'une variable, d'une constante typée ou d'une procédure/ fonction. Indépendamment du type de
2-106 La Bible Turbo Pascal
 
2- 107 Dans les coulisses du langage
 
 
l'argument, le compilateur charge l'adresse comme pointeur FAR dans les registres DX:A)( et de là il l'affecte à une variable ou la dépose sur la pile si elle doit être transmise comme paramètre de fonction ou de procédure. Cette suite d'opérations n'est pas très performante quand le compilateur connaît déjà par ailleurs l'adresse de la variable, ce qui est certainement le cas des variables globales et absolues. Le segment et le déplacement de la variable pourraient alors être affectés sans l'intermédiaire des registres A)( et DX.
 
l'argument, le compilateur charge l'adresse comme pointeur FAR dans les registres DX:A)( et de là il l'affecte à une variable ou la dépose sur la pile si elle doit être transmise comme paramètre de fonction ou de procédure. Cette suite d'opérations n'est pas très performante quand le compilateur connaît déjà par ailleurs l'adresse de la variable, ce qui est certainement le cas des variables globales et absolues. Le segment et le déplacement de la variable pourraient alors être affectés sans l'intermédiaire des registres A)( et DX.
 +
 
Quand on indique dans la fonction Addr une variable absolue ou une variable adressée par des pointeurs, Turbo Pascal copie aussi le segment et le déplacement de la variable dans les registres ES et DI. La signification de cette copie n'est pas très claire, mais il est probable que le compilateur se prépare ainsi à des accès ultérieurs à la variable.
 
Quand on indique dans la fonction Addr une variable absolue ou une variable adressée par des pointeurs, Turbo Pascal copie aussi le segment et le déplacement de la variable dans les registres ES et DI. La signification de cette copie n'est pas très claire, mais il est probable que le compilateur se prépare ainsi à des accès ultérieurs à la variable.
 +
 
Les instructions générées par la fonction Addr montrent bien la différence entre les variables globales déposées dans le segment des données et les variables locales temporairement stockées sur la pile. Dans le premier cas, le segment est le contenu du registre DS, alors que les variables locales sont dans le segment indiqué par le registre SS.
 
Les instructions générées par la fonction Addr montrent bien la différence entre les variables globales déposées dans le segment des données et les variables locales temporairement stockées sur la pile. Dans le premier cas, le segment est le contenu du registre DS, alors que les variables locales sont dans le segment indiqué par le registre SS.
 +
 
Une instruction telle que
 
Une instruction telle que
 +
 
p:— Addr(p'); I
 
p:— Addr(p'); I
 +
 
est dépourvue d'intérêt car P s'affecte son propre contenu. Le code généré par Turbo Pascal confirme ce cercle vicieux. Le contenu de P est chargé dans DX:AX (par l'intermédiaire de ES et DI) et de là il est à nouveau transféré dans P. On ne peut pas demander au compilateur d'éliminer ce genre d'absurdité, car elle ne devrait pas figurer dans un programme digne de ce nom.
 
est dépourvue d'intérêt car P s'affecte son propre contenu. Le code généré par Turbo Pascal confirme ce cercle vicieux. Le contenu de P est chargé dans DX:AX (par l'intermédiaire de ES et DI) et de là il est à nouveau transféré dans P. On ne peut pas demander au compilateur d'éliminer ce genre d'absurdité, car elle ne devrait pas figurer dans un programme digne de ce nom.
 
Programme de démonstration servant à étudier le code machine généré par la fonction ADDR de Turbo Pascal
 
Programme de démonstration servant à étudier le code machine généré par la fonction ADDR de Turbo Pascal
Ligne 2 302 : Ligne 2 342 :
 
test; .J
 
test; .J
 
end.
 
end.
VChr
+
===Chr===
 
Lorsque des programmeurs en langage C touchent pour la première fois à Pascal, ils sont toujours désagréablement surpris par la stricte séparation entre les types CHAR et INTEGER qui n'existe pas en C. Après tout, les types CHAR et BYTE ne sont-ils pas l'un et l'autre stockés dans un octet, alors pourquoi est-il interdit de procéder à des affectations réciproques? Bien que rien ne s'y oppose sur le plan de la technique de programmation, un tel mélange serait contraire à la philosophie de Pascal. On dirait presque de l'apartheid, mais heureusement Turbo Pascal ouvre quand même quelques possibilités de communication.
 
Lorsque des programmeurs en langage C touchent pour la première fois à Pascal, ils sont toujours désagréablement surpris par la stricte séparation entre les types CHAR et INTEGER qui n'existe pas en C. Après tout, les types CHAR et BYTE ne sont-ils pas l'un et l'autre stockés dans un octet, alors pourquoi est-il interdit de procéder à des affectations réciproques? Bien que rien ne s'y oppose sur le plan de la technique de programmation, un tel mélange serait contraire à la philosophie de Pascal. On dirait presque de l'apartheid, mais heureusement Turbo Pascal ouvre quand même quelques possibilités de communication.
 
L'une d'elles est la fonction Chr qui convertit une expression entière en un opérande de type Char. Du point de vue interne, les transformations nécessaires sont tout à fait minimes comme le montre le programme suivant qui affecte le code ASCII 12 à une variable de type Char. La valeur 12 est ici une constante entièrement résolue au moment de la compilation. Par conséquent, le code est directement chargé dans la variable. Il est difficile de faire plus efficace.
 
L'une d'elles est la fonction Chr qui convertit une expression entière en un opérande de type Char. Du point de vue interne, les transformations nécessaires sont tout à fait minimes comme le montre le programme suivant qui affecte le code ASCII 12 à une variable de type Char. La valeur 12 est ici une constante entièrement résolue au moment de la compilation. Par conséquent, le code est directement chargé dans la variable. Il est difficile de faire plus efficace.

Version du 1 juillet 2020 à 11:20

Nous allons nous rendre maintenant derrière la scène d'un programme écrit en Turbo Pascal pour faire connaissance avec les acteurs et leurs accessoires. Nous intéresserons avant tout à la structure de base d'un tel programme, au déroulement des fonctions et procédures ainsi qu'à la mémorisation des variables. Vous découvrirez les particularités de l'unité System et vous pourrez jeter un coup d'oeil sur l'organisation du tas (heap). Il ne s'agit pas d'être simplement curieux. La connaissance du mécanisme de fonctionnement d'un programme Turbo Pascal est indispensable pour comprendre de nombreuses manipulations présentées dans les chapitres qui suivent. Il n'est pas possible de réaliser des programmes résidents, de gérer des stockages de programmes temporaires ou de réaliser une unité multitâche sans ces notions. Laissez vous guider à travers ce monde fascinant de bits et d'octets.

Le modèle mémoire de Turbo Pascal

La première question qui se pose quand on commence à parcourir les coulisses d'un programme est de savoir où se trouvent ses différents éléments: code, données, pile et tas. En d'autres termes on cherche à définir le modèle mémoire que le compilateur utilise quand il transforme les instructions Pascal en langage machine.

Turbo Pascal fait tout pour simplifier cette recherche: car à l'inverse du langage C par exemple il ne connaît qu'un seul modèle mémoire auquel il se tient invariablement. Cette intransigeance limite certes la liberté du programmeur, mais elle lui évite aussi les tourments d'un choix délicat (les débutants qui se frottent à C en savent quelque chose).

Le modèle mémoire de Turbo Pascal est assez différent de ceux qui sont habituellement pratiqués par les autres compilateurs ( Seul Quick Pascal imite ce modèle pour des raisons de compatibilité). Si on cherche à tout prix une analogie, on pourra néanmoins le rapprocher du modèle MEDIUM défini par C. Un programme en Turbo Pascal contient de multiples segments de code, chacun d'eux pouvant épuiser la limite des 64 Ko inhérente au processeur 8086/8088. Le programme principal possède son propre segment, ainsi que chaque unité. Comme un programme peut contenir un nombre quelconque d'unités, la taille du code n'est limitée que par l'espace mémoire disponible et peut aller jusqu'à I Mo. Chaque programme possède au moins deux segments de-code : le segment du programme principal et celui de l'unité System que Turbo Pascal incorpore systématiquement sans même qu'on le demande. Quand un programme Turbo vient d'être chargé par le loader EXEC de DOS, les segments de code se situent tout à fait au début. Leur ordre n'est pas quelconque, mais obéit à des règles strictes. Le segment de code du programme principal précède les segments de code des unités qui apparaissent dans l'ordre inverse de leur énumération par USES.

Si le programme principal contient l'instruction:

Uses Uniti, Unit2. Unit3;

le segment de code du programme principal est suivi par le segment de code de Unit3, puis par Unit2 et Uniti. Le dernier segment de code est toujours celui de l'unité System. Le premier segment de code est précédé par une structure appelée PSI' (Program Segment Prefix) ou préfixe du segment du programme. La présence du PSI' n'est pas due à Turbo Pascal mais au loader EXEC. Chaque fois que la loader charge un programme EXE, il le fait précéder en mémoire par le PSI'. Cette zone de données contient en fait d'importantes informations utilisées par le système d'exploitation DOS pour faire fonctionner le programme.

Alors que la taille maximale du code paraît pratiquement illimitée, l'espace mémoire dévolu aux données paraît en contrepartie tout à fait mesquin: le segment de données est en effet limité à 64 Ko. L'inquiétude s'accroît quand on apprend que ce segment doit contenir non seulement les variables globales, mais également toutes les constantes typées aussi bien celles du programme principal que celles des unités incluses. Mais cette inquiétude n'est pas justifiée les 64 Ko s'avèrent en général plus que suffisants, et si jamais la place se faisait trop juste à l'intérieur du segment de données, il serait toujours possible de recourir au tas (heap). Ce dernier peut en général offrir plusieurs centaines de Ko -selon la quantité de mémoire installée et la place que prend déjà le code du programme concerné. Par ailleurs le tas se prête parfaitement à l'implémentation de structures dynamiques du genre arbre ou listes chaînées. Au demeurant ces objets particulièrement sympathiques, rendus populaires par le célèbre ouvrage de Niklaus Wirth ("Algorithmes et structures de données"), ne devraient manquer dans aucun programme en Pascal digne de ce nom.

Pour que les constantes typées soient toujours à la disposition immédiate de Turbo Pascal, le registre DS pointe constamment sur le segment de données correspondant tout au long de l'exécution du programme. L'adresse de ce segment, c'est-à-dire le contenu du registre DS, peut être connue à tout moment au moyen de la variable prédéfinie DSeg.

Juste après le segment de données se trouve la pile qui est destinée à stocker temporairement les adresses de retour des sous-programmes et les variables dites locales. Sa taille par défaut est de 16 Ko (16384 octets). A l'intérieur de l'EDI (environnement de développement intégré) elle peut être paramétrée par l'option "Compiler/Memory" du menu "Options". A l'intérieur des programmes elle peut être modifiée par la directive de compilation ($M). Pendant l'exécution d'un programme, le registre SS pointe constamment sur le segment de pile. Son adresse est contenue dans la variable prédéfinie SSeg.

Si SS reste constant, il n'en est pas de même du registre SP qui varie considérablement au cours d'une exécution. Au début du programme, SP pointe sur la mémoire qui se situe juste après le dernier octet du segment de la pile. A chaque appel de sous-programme, à chaque transmission de paramètres aux fonctions et procédures, le pointeur SP se déplace vers le début ou la fin du segment de pile. Sa position courante peut être obtenue en examinant le contenu de la variable SPtr. Tout comme SSeg, DSeg et CSeg, cette variable est déclarée automatiquement par l'unité System. Vous apprendrez dans la suite de cet ouvrage comment manier la pile en fonction des variables locales stockées et des paramètres transmis aux fonctions et aux procédures.

A la fin du segment de pile commence la zone du tas (heap). Le tas permet l'allocation dynamique de mémoire au moyen des procédures New et GetMem. Si sa taille n'est pas limitée par la directive de compilation ($M) ou par l'option "Compiler/Memory" du menu "Options", le tas utilise toute la mémoire disponible jusqu'à la frontière des 640 Ko.

Entre le segment de pile et le tas, Turbo Pascal intercale le tampon des recouvrements (overlay buffer) lorsqu'un programme travaille avec des overlays (ou recouvrements) et qu'il inclut à cet effet l'unité Overlay dans une instruction USES. Le tampon se réserve la place suffisante pour contenir le plus grand des fichiers de recouvrement, et par conséquent tous les autres qui viendraient s'y substituer et qui sont nécessairement plus petits.

Le PSP

Le PSP est une structure de données mise en place par le loader EXEC qui à l'intérieur du noyau de DOS est responsable du chargement des programmes COM et E)Œ. Comme son nom l'indique, le PSI' est disposé en tête du programme dans la mémoire centrale. Il occupe 256 octets (soit 16 paragraphes) et se situe, dans le cas d'un programme compilé en Turbo Pascal, juste avant le segment de code. Si l'adresse du segment de code est par exemple $3Fb, le PSP commence au segment d'adresse $3F00. Mais à vrai dire vous n'êtes pas censé vous livrer à ce genre de calcul car Turbo Pascal dispose l'adresse du segment du PSP dans une variable prédéfinie appelée PrefixSeg.

Adresse tontenu Type $00 Appel de l'interruption 20h 2 BYTE $02 Adresse du dernier segment occupé par le programme 1 WOlI) $04 Réservé 1 BYTE $05 Appel FAR de l'interruption 21 h 5 BYTE $OA Copie du vecteur d'interruption 22h 1 PTR $OE Copie du vecteur d'interruption 23h 1 PTR $12 Copie du vecteur d'interruption 24h 1 PTR $16 Réservé 22 BYTE $2C Adresse du segment du bloc d'environnement 1 WOFD $2E Réservé 46 BYTE $5C FCB#1 16 BYTE $6C FCB#2 16BYTE $80 Nombre de caractères de la ligne de commande 11 BYTE (excepté le Retour Chariot terminal) $81 Ligne de commande 127 BYTE

Longueur totale: 256 octets

Figure 14 Structure du PSP (Préfixe du Segment du Programme)

Le système DOS utilise le PSI' pour gérer le programme. Il s'y trouve quelques informations qui servent dans certains cas à "tricher" un peu, c'est-à-dire à sortir des règles imposées par la syntaxe de Turbo Pascal. Tous les champs n'ont pas la même utilité car la structure du PSP n'a pas évolué depuis la version 1.0 de DOS alors que les développeurs de Microsoft ont largement modifié-le noyau du système.

C'est ainsi qu'on trouve en tête du PSI' un appel à l'interruption 20h qui servait autrefois à clôturer un programme mais qui n'est plus utilisée aujourd'hui car elle est remplacée par une autre fonction.

Le deuxième champ est beaucoup plus intéressant: il indique à DOS l'adresse du segment de la dernière mémoire allouée au programme.

Le troisième champ porte le titre "réservé" ce qui veut dire en fait "non documenté". De nombreux programmeurs se sont attachés à décrypter la signification de ce type de champ, et certains d'entre eux y sont même parvenus. Mais cette signification peut être modifiée sans préavis d'une version de DOS à l'autre. De toute façon, nous n'en parlerons pas car la programmation en Turbo Pascal n'est pas concernée.

A l'adresse 5 se trouve un appel au répartiteur de fonction qui se cache derrière l'interruption 21h et qui coordonne l'aiguillage des fonctions de DOS gérées par cette interruption. Cette indication est également obsolète et dénuée de tout intérêt.

Par contre les trois champs suivants ont plus d'importance : le système DOS y mémorise les contenus de trois vecteurs d'interruption qui sont souvent "détournés" par les programmes et qui doivent donc être rétablis à leur valeur initiale à la fin de l'exécution. L'interruption 22h sert à clôturer un programme et est invoquée par toutes les fonctions DOS que le programmeur met en service pour terminer un programme. L'interruption 23h est une routine déclenchée par la combinaison des touches "Contrôle C" et qui sert à interrompre brutalement mais en toute régularité le programme en cours d'exécution. L'interruption 24h pointe sur une routine que DOS appelle en cas d'erreur critique. C'est elle qui émet par exemple le célèbre message "Abandon, Reprise, Ignore?".

Comme nous le verrons, Turbo Pascal détourne les deux dernières interruptions citées pour substituer aux routines habituelles de DOS ses propres routines internes issues de l'unité System. Il est indispensable d'employer de tels moyens pour réaliser par exemple un contrôle indépendant des entrées-sorties et empêcher qu'un programme soit interrompu par "Ctrl C".

Les trois vecteurs d'interruption sont suivis par un champ réservé, après quoi on trouve l'adresse du segment du bloc d'environnement. Des explications détaillées sur ce bloc vont vous être données au paragraphe suivant.

Après l'adresse du bloc d'environnement on trouve encore un certain nombre de champs non documentés. Puis le PSP prévoit de la place pour deux FCB installés automatiquement par DOS. "FCB" est un acronyme pour "File Control Block, une structure de données que DOS utilisait jusqu'à la version 2.0 pour accéder aux fichiers. Depuis la version 2.0 les fonctions du FCB ont été remplacées par les fonctions de "handle" qui sont plus faciles à utiliser pour accéder aux fichiers et aux périphériques. De nos jours les FCB ne jouent plus aucun rôle dans la programmation des PC. Les deux derniers champs du PSP contiennent par contre des informations extrêmement importantes: il s'agit en effet des arguments de la ligne de commande. Le loader EXEC mémorise à partir de l'offset $81 tous les caractères qui suivent le nom du programme appelé. Sont exclus les indications servant au détournement des entrées-sorties (> et <) et les arguments liés à l'enchaînement (I) de plusieurs instructions. La longueur de la chaîne jusqu'au Retour Chariot (Code ASCII 13), autrement dit le nombre des caractères qui constituent les arguments, est stockée à l'adresse de l'offset $80. Pour être précis, signalons que le Retour Chariot n'est pas pris en compte dans l'estimation de la longueur de la chaîne car il ne provient pas de la ligne'de commande mais a été rajouté par le loader EXEC.

Comme il n'existe pas d'autre possibilité de connaître ces informations, Turbo Pascal lit les champs correspondants au début du programme principal. Le nombre d'arguments décelés est transmis à la variable ParamCount. Les arguments eux-mêmes sont accessibles au moyen de la fonction ParamStr.

Le bloc d'environnement

Le loader EXEC écrit dans le PSP une copie du bloc d'environnement gérée par l'interpréteur de commandes COMMAND.COM. Il s'agit d'une collection de chaînes de caractères dans laquelle l'interpréteur de commandes et d'autres programmes trouvent diverses informations, comme par exemple le chemin d'accès aux fichiers exécutables (PATH). Chacune des chaînes qui constituent le bloc a normalement la forme:

Nom-Paramètre	I

et se termine par un caractère NUL (de code ASCII 0). Les chaînes sont disposées bout à bout, de sorte que le caractère terminal de l'une est suivi du premier caractère de la suivante. La fin du bloc d'environnement, dont la longueur est limitée à 32 Ko, est signalée par un caractère de clôture qui suit le caractère terminal de la dernière chaîne.

A partir de la version 3.0 de DOS, la dernière chaîne du bloc d'environnement peut être suivie d'autres chaînes. Le nombre de ces chaînes supplémentaires est alors indiqué sous la forme d'un mot placé juste après la dernière chaîne du bloc d'environnement. A l'heure actuelle, DOS ne gère qu'une seule chaîne supplémentaire, le mot en question contient donc la valeur 1. La chaîne supplémentaire contient la désignation de l'unité et le chemin d'accès complet du programme en cours d'exécution, c'est-à-dire de celui auquel appartient le PSP. Grâce à cette information contenue dans le bloc d'environnement étendu, un programme peut par exemple détecter le répertoire d'où il a été lancé et rechercher automatiquement les overlays, fichiers de configuration et autres fichiers annexes dans le même répertoire sans avoir à interroger l'utilisateur à ce sujet.

Au niveau de l'utilisateur, ce sont les commandes SET et PATH de DOS qui servent à manipuler le bloc d'environnement. Ces commandes permettent d'y ajouter ou d'en modifier une chaîne.

En exploitant l'adresse du segment du bloc d'environnement située au déplacement $2C du PSP, Turbo Pascal se ménage un accès aux informations déposées à cet endroit. L'unité DOS dispose à cet effet des fonctions EnrStr et GetEnv, tandis que le nombre des paramètres est tenu à jour dans la variable EnvCount.

Il est intéressant de noter que la dernière chaîne qui donne le nom et le chemin d'accès du programme exécuté n'est pas prise en compte à te niveau. Mais elle est retournée au niveau de l'unité System par la fonction ParamStr munie de l'argument O.

Constantes typées et constantes non typées

En Turbo Pascal les constantes typées sont stockées avec toutes les autres variables locales dans le segment commun des données. Cette disposition n'est guère etonnante, car les constantes typées ne se distinguent des variables ordinaires que par le fait qu'elles sont initialisées par le programmeur, alors que les autres variables ont une valeur indéterminée au début de l'exécution du programme. Les constantes typées déclarées localement dans une procédure ou une fonction sont également stockées dans le segment des données, quoique dans ce cas elles ne soient accessibles que de l'intérieur de la procédure ou de la fonction en question.

Malgré tout, les constantes typées ne sont pas mélangées aux variables globales à l'intérieur du segment des données. Elles se retrouvent ensemble au début du segment, sous la forme d'un grand bloc commun. Turbo Pascal les range à cet endroit pour économiser de la place mémoire à l'intérieur du fichier exécutable compilé. Le fichier E)(E, en effet, ne doit contenir que les éléments strictement indispensables au lancement du programme. Parmi ces éléments indispensables figure évidemment le code machine du programme, mais aussi les constantes typées qui doivent avoir une valeur initiale.

Le contenu des variables globales est par définition indéterminé au moment du lancement du programme. C'est ainsi que ce dernier contient des références à ces variables, mais les variables elles-mêmes ne sont pas stockées: une place est prévue pour elles juste derrière les constantes typées.

Pour ce qui est des constantes non typées déclarées par l'instruction CONST, elles occupent parfois de la mémoire mais pas toujours. Cela dépend de leur type. En principe Turbo Pascal n'est obligé de réserver de la mémoire que pour les constantes référencées par des pointeurs. Pour les nombres, c'est-à-dire les types Integer et Real, cette remarque n'est pas valable car Turbo Pascal peut les transférer par des instructions MOV dans les registres du processeur ou les variables. Mais il en est autrement des chaînes de caractères. Celles-ci doivent être effectivement déposées dans la mémoire car Turbo Pascal les adresse uniquement par des pointeurs. Et il va de soi qu'un pointeur ne peut référencer qu'un objet qui se trouve réellement et physiquement en mémoire. Les constantes chaînes de caractères sont d'ailleurs plus nombreuses que vous ne le pensez a priori. Elles ne sont en effet pas seulement générées par des instructions du type:

const Message - Salut Turbo;	I

Des instructions comme

Iwriteln('Constante chaîne');	1

ou

chaine–'Texte'	I

donnent automatiquement naissance à des constantes chaînes sans nom, les chaînes correspondantes devant être adressées comme les autres.

Mais contrairement à ce qu'il fait pour les constantes typées, Turbo Pascal ne range pas ces constantes chaînes dans le segment des données. Il les place dans le segment du code, juste avant les procédures ou fonctions qui y font appel.

Les constantes chaînes qui apparaissent à plusieurs reprises dans une procédure ou une fonction ne sont stockées qu'une seule fois à un même endroit. Le listing qui suit assorti de l'extrait de mémoire correspondant montre le fonctionnement de Turbo Pascal dans ce domaine.

Prograrn Constantes;.J				
const Salutation–'Salut' ;.J				
Enti erLong–$11112222 ;.J				
EntierNormal–S4711 ;.i				
NombreReel-1 .2345678;_J				
Procedure Test;.J				
 var	r	real;.J				
 1	:	longlrit;.J				
 I	:	integer;.J				

begin.J writeln( Salutation );.J cs:001C BF3EO1 mov dl.013E ;Séquence ordinaire Writeln ).J ( cs:001F lE push ds ).J j cs:0020 57 push dl j cs:0021 BF0000 mov di,0000 ; <--- Attentioni j cs:0024 0E push cs ; la chaîne commence Li cs:0025 57 push di ; à l'adresse CS:0000 ).j j cs:0026 31C0 xor ax.ax Li j cs0028 50 push ax Li j cs:0029 9A26069E5F call 5F9E:0626 Li j cs:002E 9AA9059E5F call 5F9E:05A9 '-J writeln( • Turbo • cs:0033 BF3EO1 mov di .013E ;autre Writeln Li j cs:0036 lE push ds ( cs:0037 57 push di ).j j cs:0038 BF0600 mov di.0006 ;cette chaîne commence en Li cs:0038 0E push cs ;CS:0006. elle vient donc j cs:003C 57 push di ;,juste après la première ).J j cs:0030 31C0 xor ax,ax j cs:003F 50 push ax j cs:0040 9A26069E5F call 5F9E:0626 j cs:0045 9AA9059E5F call 5F9E:05A9 'J writeln( Salutation );.J j cs:004A BF3EO1 mov di .013E ;Writeln Li cs:0040 lE push ds L-i cs:004E 57 push di Li cs:004F 8F0000 mov dl.0000 ;Turbo sait qu'il a déjà U cs:0052 0E push cs ;stocké cette chaîne et remet ).j — cs:0053 57 push di ;en service l'adresse CS:0000 ).J Dans les coulisses du langage 2-43 La Bible Turbo Pascal 2-44 C cs:0054 31C0 xor ax.ax f cs:0056 50 push ax ( cs:0057 9A26069E5F call 5F9E:0626 ( cs:OOSC 9AA9059E5F call 5F9E:05A9 -J I :- EntierNormal;.J f cs:0061 C746F4E803 mov word ptr [bp-OC].4711 ;charge un mot ).i J 1 :- EntierLong;.J cs:0066 C746F64042 mov word ptr [bp-OA],2222 ;les deux mots qui ).i f cs:006B C746F80F00 mov word ptr [bp-08].1111 ;composent un entier long ).J r :- NonibreReel;.J cs:0070 C746FA81D9 mov word ptr [bp-06].0981 :charge un réel ).J cs:0075 C746FC5251 mov word ptr [bp-04],5152 ;qui prend 6 octetsLi f cs:007A C746FE061E mov word ptr [bp-02].1E06 end ;.J -J begi n.J wrlteln( Salutation );.J cs:0099 BF3EO1 mov di.013E ;séquence ordinaire Wrlteln ).J f cs:009C lE push ds f cs:009D 57 push di cs:009E BF8300 mov di,0083 ;bien que Turbo Pascal ait déjà)J cs:OOA1 0E push cs ;stocké cette chaîne, elle est )J cs:00A2 57 push dl ;générée une deuxième fois à ).J f cs:00A3 31CO xor ax.ax ;l'adresse CS:0083 f cs:00A5 50 push ax C cs:00A6 9A26069E5F call 5F9E:0626 f cs:OOAB 9AA9059E5F call 5F9E:05A9

wrlteln( Turbo ).J f cs:OOBO BF3EO1 mov dl.013E ;dito f cs:00133 lE push ds f cs:00134 57 push di f cs:0085 BF8900 mov di,0089 ;cette chaîne est également ).i f cs:00138 0E push es ;répétée et disposée devant ).j f cs:0089 57 push di ;la procédure C cs:OOBA 31C0 xor ax.ax Li f cs:OOBC 50 push ax C cs:OOBD 9A26069E5F call 5F9E:0626 f cs:00C2 9AÀ9059E5F call SF9E:05A9 test ;.J end. 6CAB:8888 85 53 61 6C 75 74 07 28 54 75 72 62 6F 28 55 09 Salut. Turbo Ue 6CAII:0810 E5 BU OC 88 9A 44 82 BA 6C 03 EC OC 11F 3E 81 lE w19 lâ-q 6CAB:8020 57 11F 80 80 (lE 57 31 CO 58 9A 26 86 BA 6C 9 A9 U1 J1J1LPii+fllU,- 6CAB:8030 OS BA £C 9A 0E 82 BA 6C 8F 3E 81 lE 57 11F 86 8 6CAB:0040 8E 57 31 CO 50 9A 26 86 BA 6C 9A A9 85 BA 6C 9A JJ1LpU&+fl1ii7.O11j 6CAB:0050 8E 82 BA 6C BF 3E 01 lE 57 BF 00 00 0E 57 31 COfl0UL1>LU1 [XJlL 6CAB:0060 50 9A 26 06 BA 6C 9 A9 OS BA LC 9A 0E 02 BA 6C Pii&+fllU-.DIUrJ8IIl 6CAB:0070 C? 46 F4 11 47 C? 46 F6 22 22 C? 46 F8 11 ii C? VFNCIIF+'"11r4IU 6CAB:0080 16 FA 81 D9 C? 46 FC 52 51 C? 46 FE 06 lE 89 EC 6CAB:0090 SD C3 05 53 61 6C 75 74 87 28 54 75 72 62 6F 20 ]I48alut. Turbo Figure 16: Début du segment de code du programme précédent. Le double stockage des constantes chaîne 'Salut' et 'Turbo est aisément reconnaissable.

Le listing montre effectivement que Turbo Pascal ne stocke pas en mémoire les constantes numériques mais qu'il les charge à chaque fois dans la variable concernée au moyen de l'instruction MOV. A l'inverse, les chaînes de caractères sont quant à elles systématiquement stockées en mémoire. Lorsque des chaînes identiques sont appelées par des procédures différentes, leur stockage est répété. Mais cette redondance est évitée lorsque la même chaîne réapparaît plusieurs fois au sein de la même procédure ou fonction.

Cette façon de procéder peut paraître au premier abord illogique et inefficace, mais elle est en fait inévitable compte tenu du fonctionnement de l'éditeur de liens interne lorsqu'il relie le programme aux unités incluses. Le Turbo Linker est en effet suffisamment intelligent pour n'incorporer que le strict nécessaire, c'est-à-dire qu'il ne prend pour constituer le fichier exécutable que les procédures, fonctions et variables réellement appelées ou utilisées à l'intérieur du programme. Mais ces décisions sont encore prématurées au moment de la compilation, surtout pour ce qui concerne les unités. Une procédure P2 ne peut pas accéder aux constantes chaînes de la procédure PI cari! est possible que PI ne soit jamais invoqué et ne soit pas incorporé dans le fichier EXE. En cherchant la constante, la procédure P2 risquerait de ne rien trouver, ce qui conduirait au moment de l'édition des liens à une erreur "unresolved external".

Variables

Types d'entiers ....................................................46 Types énumérés ...................................................46 Booléens..........................................................46 Caractères Char ...................................................47 Chaînes de caractères String ........................................47 Tableaux..........................................................47 Enregistrements Record ............................................48 Ensembles........................................................48 Pointeurs.........................................................48 Types réels (virgule flottante) .......................................49 Alignement des variables ..........................................49

Turbo Pascal stocke les variables globales dans le segment de données et les variables locales sur la pile (voir la section consacrée aux procédures et fonctions).

Cet énoncé lapidaire est insuffisant pour traiter le sujet car il nous faut répondre à d'autres questions: de quelle manière Turbo Pascal procède-t-il au stockage, quel est le format utilisé, comment les variables sont-elles traitées? Le présent chapitre se propose justement de répondre à ces questions.

Bien entendu Turbo Pascal n'est pas libre de choisir pour chaque type de donnée le format qui lui plaît. Mais il va s'efforcer de générer des programmes aussi rapides que possible en tenant compte des propriétés du processeur. Ce dernier ne sait pas ce qu'est une chaîne, une variable reelle ou booléenne, mais son jeu d'instructions détermine évidemment la manière dont vont être traitées les informations.

Turbo Pascal répartit les différents types en plusieurs groupes auxquels il associe divers formats de données.

Types d'entiers

Parmi les types d'entiers, Turbo Pascal distingue des mémorisations sur un, deux ou quatre octets. Au niveau du processeur les types correspondants sont BYTE, WORD et DWORD.

Note: Pour distinguer les formats du processeur des formats de Turbo Pascal, nous écrirons toujours les premiers en lettres capitales.

Pour stocker des octets, Turbo Pascal propose les types Byte et Shortlnt. La différence entre ces deux types tient à l'interprétation de leurs valeurs. Byte est un type de données non signé et sert donc à représenter un nombre compris entre O et 255. A l'inverse, le bit 7 d'un type Shortlnt est interprété comme un signe, ce qui étend le domaine de définition sur l'intervalle -128 à 127.

La même distinction entre types signé et non signé s'applique aux entiers stockés sur deux octets et gérés au niveau machine par le format WORD. Turbo Pascal propose ainsi les deux types de données Word et Integer. Le type Integer est signé et son intervalle de définition va de -32768 à 32767. Les valeurs prises par un entier de type Word ne sont jamais négatives et vont de O à 65535.

Si l'intervalle de définition n'est pas encore suffisamment étendu, il faut faire appel au type Longlnt. Au niveau interne il correspond à un DWORD sur 4 octets interprété sous forme signée. L'intervalle de définition va de -2147483648 à 2147483647. Pour mémoriser les entiers Longlnt, Turbo Pascal suit les conventions posées par INTEL pour les mots doubles DWORD. Le mot de poids inférieur est mis dans la mémoire d'adresse basse, le mot de poids supérieur dans celle qui suit.

Types énumérés

Les types énumérés sont codés de façon que le premier élément reçoive le rang O, le deuxième le rang 1, et ainsi de suite. Tant que le nombre d'éléments est inférieur à 257, le type énuméré est géré sous la forme d'un octet BYTE. Si le nombre d'éléments est supérieur, le stockage utilise un entier WORD.

Booléens

Au niveau interne, les variables booléennes sont stockées sous forme BYTE quoiqu'elles ne puissent prendre que les valeurs O (false) ou 1 (true). On peut se remémorer cette définition en considérant le type énuméré équivalent:

type boolean - (VALSE. TRUE); I

qui fournit les valeurs

ord(FALSE) et ord(TRUE)	I

c'est-à-dire O ou 1.

Caractères Char

Les caractères sont mémorisés comme des octets BYTE (non signés).

Chaînes de caractères String

Les chaînes String sont des suites de caractères ordonnés. L'un des caractères contient la valeur de la longueur actuelle de la chaîne. Cette information ne doit pas être confondue avec la longueur déclarée de la chaîne qui n'est pas mémorisée à l'intérieur de celle-ci.

Comme la longueur d'une chaîne occupe un octet, une chaîne ne peut pas dépasser 255 caractères: au maximum on aura donc 256 octets en incluant l'octet qui donne la longueur. Pour les chaînes déclarées sans longueur la mémoire réservée est toujours de 256 octets. Pour économiser de la mémoire, il vaut mieux indiquer une longueur maximale au moment de la déclaration lorsqu'on est en mesure de la prévoir.

Une chaîne peut être adressée comme un tableau de caractères Char avec un indice compris entre O et la longueur de la chaîne. L'élément d'indice O renvoie la longueur actuelle de la chaîne, à l'indice I correspond le premier caractère de la chaîne. Ce format est très efficace et certains fabricants de logiciels (comme Microsoft) l'utilisent même avec le langage C, alors même que ce langage prévoit une autre représentation!

La possibilité d'accéder directement à l'octet définissant la longueur de la chaîne permet quelques astuces de manipulation pour accélérer l'exécution d'un programme. Le listing suivant montre quelques-unes des techniques envisageables:

Program Astuces;.J var chaine : string; f Chaine de long. max. 255 caractères )J der: char;J beg in.-' chaine:— Chaîne de test';.-i der:— chaine[ord(chalne[O])]; E prend le dernier caractère ).j dec(chalne[O]); C enlève le dernier caractère )J inc(chaine[O]); C rajoute un point d'exclamation en chalne(ord(chaine[O])]:— 1; ( bout de chaIne )J writeln(chalne);.J end.

Notez bien que vous ne pouvez pas utiliser directement la longueur de la chaîne comme indice, car du point de vue de Turbo Pascal cette longueur est constituée par un caractère, c'est-à-dire un type de donnée impropre à indicer une chaîne. Mais la fonction Ord permet de transformer un caractère Char en un octet Byte qui est, quant à lui, susceptible de servir d'indice.

Tableaux

Les tableaux sont stockés sous la forme de leurs éléments juxtaposés. L'élément dont l'indice est le plus petit est rangé dans la mémoire dont l'adresse est la plus basse. Il n'y a pas d'espace entre les éléments qui sont mis bout à bout.

Dans les tableaux multidimensionnels ce sont les éléments de la dernière dimension qui se retrouvent côte à côte. Ainsi le tableau

a—array[1. .1O][1. .5] 0f char	I

présente en mémoire la disposition suivante: d'abord les éléments a[1][1] à a[1][5], puis les éléments a[2][1] à a[2][51, et ainsi de suite.

Enregistrements Record

Du point de vue du stockage, Turbo Pascal traite les enregistrements comme les éléments d'un tableau. Les différents champs sont donc mis bout à bout dans l'ordre de leur déclaration. Le premier élément se trouve à l'adresse la plus basse, le dernier à l'adresse la plus haute. A l'intérieur d'un enregistrement les parties variables (introduites par CASE) se recouvrent.

Ensembles=

Toujours soucieux de consommer le moins de mémoire possible, Turbo Pascal range les ensembles sous forme de tableaux de bits, chaque bit représentant un élément de l'ensemble. Si le bit est à un, l'élément appartient à l'ensemble; si le bit est à zéro, il n'en fait pas partie. La place mémoire utilisée dépend du nombre des éléments et par conséquent du rang ordinal du dernier et du premier élément (appelés ici Omin et Omax), selon la formule: Nb octets - (Omax div 8) - (Omin div 8) + 1

Pointeurs

En Turbo Pascal les pointeurs sont essentiellement de type FAR. Ils se composent donc d'une adresse de segment et d'un déplacement qui sont tous deux dans le format WORD. Selon la convention prescrite par INTEL, le déplacement (offset) précède l'adresse du segment en mémoire centrale.

Pour accéder aux variables globales et aux constantes typées du segment de données, Turbo Pascal pourrait également mettre en service des pointeurs NEAR, puisque la zone mémoire concernée ne dépasse pas 64 Ko et qu'elle peut être adressée en permanence par le registre DS. Mais dans ce cas, le tas (heap) deviendrait inaccessible à ces pointeurs. Or le tas joue un rôle important dans la programmation en Turbo Pascal et il n'est pas concevable qu'une distinction s'opère à ce niveau entre les pointeurs de données ordinaires et les pointeurs de tas. C'est la raison pour laquelle Turbo Pascal utilise les pointeurs FAR. Toute la mémoire de I Mo disponible sur un PC est ainsi accessible. Le pointeur appelé NIL joue un rôle particulier. L'adresse de son segment et son déplacement valent tous deux O. Il pointe donc sur la première entrée de la table des vecteurs d'interruption et ne doit pas être utilisé pour référencer des données.

Les pointeurs de procédures et de fonctions, c'est-à-dire les types procéduraux, sont également stockés sous la forme FAR. Ici il n'y a pas d'alternative possible car tout 2-48 La Bible Turbo Pascal 2-49 Dans les coulisses du langage programme est réparti dans de multiples segments de code, et l'appel des fonctions et des procédures doit franchir les limites du programme principal et des unités. V Types réels (virgule flottante) Pour manier les nombres en virgule flottante, le Turbo Pascal standard ne connaît que le type Real. Mais si on met en service un coprocesseur arithmétique ou une bibliothèque d'émulation, on peut utiliser trois autres types de données qui fonctionnent avec davantage de précision. Je ne voudrais pas m'étendre sur les formats internes de ces types, car leur étude est très ardue. Les règles de stockage et de combinaison des réels sont plutôt du domaine d'un cours de "mathématiques numériques". Il est toutefois important de se souvenir que le type standard Real est mémorisé sur six octets.

Alignement des variables

Avec la technique d'alignement des variables on cherche à influencer le rangement des variables de façon à les rendre plus rapidement accessibles. Il faut se rappeler à ce sujet que le 8086 et ses successeurs ont plus de facilité à accéder à des variables de type WORD et DWORD (en Pascal: Word et Longlnt) lorsqu'elles sont stockées à des adresses paires. L'accès aux adresses impaires est plus lent parce que dans ce cas le processeur doit exécuter plusieurs lectures pour charger ou stocker les différents octets qui composent un opérande. Il faut une coordination spéciale entre le processeur et le bus de données à 16 bits qui relie le processeur à la mémoire centrale. Les systèmes pourvus d'un 8088 ne tirent aucun profit des techniques d'alignement car ce processeur ne travaille qu'avec un bus à 8 bits. Ainsi les variables traitées, qu'elles soient de format WORD ou DWORD, sont de toute façon découpées en octets.

L'alignement des variables est commandé par l'option "Options/Compiler/Align Data" du menu "Options". On peut alors choisir entre les modes BYTE et WORD. En mode BYTE, aucun alignement n'est effectué. Chaque variable est simplement disposée à la suite de la précédente. En mode WORD, toutes les variables qui occupent deux ou quatre octets sont décalées vers une adresse paire lorsque les circonstances naturelles les feraient arriver à une adresse impaire. Chaque fois que ce décalage est appliqué, un espace libre d'un octet sépare la nouvelle variable de la précédente.

L'alignement peut aussi être commandé de l'intérieur d'un programme par la directive de compilation i$A+) / ($A-). En activant ou en désactivant la directive, on peut aligner sélectivement un groupe de variable dans une portion de programme. Les membres des structures ne sont pas touchés, car ils sont toujours mémorisés de façon contiguë pour ne pas gaspiller de place mémoire. La même remarque s'applique aux éléments des tableaux.

Le programme suivant montre comment le mode d'alignement influence le rangement des variables. Compilez et exécutez d'abord ce programme avec le mode d'alignement WORD. Puis refaites les mêmes opérations avec le mode BYTE. Vous verrez comment Turbo Pascal déplace les variables. Dans les coulisses du langage 2-49 La Bible Turbo Pascal 2-50 program allgne;.J type enreg - record.J wi: word;.J bi: byte;J w2: word;.J b2: byte;.-' li: longint;J end;J var wi: word;.J bi: byte;.J w2: word;.J b2: byte -_J li: longint;.J S : enreg;.I beginj writeln;J writeln('Les variables sont-elles en mode d'alignement WORD ?');.J wrlteln('Wi: , not(odd(ofs( wi ))) );.-J wrlteln('Bl: ', not(odd(ofs( bi ))) );.J writeln('W2: '. not(odd(ofs( w2 ))) );.-i wrlteln('82: ' , not(odd(ofs( b2 ))) );J wrlteln('Li: ' . not(odd(ofs( li ))) );.-i wrl tel n;J writeln('Les champs de lenregistrement sont-ils ');.J writeln('en mode dalignement WORD ?');J writeln('s.wi: . not(odd(ofs( s.wi ))) );J writeln('s.bi: . not(odd(ofs( s.bi ))) );.J writeln('s.w2: , not(odd(ofs( s.w2 ))) );J writeln('s.b2: ', not(odd(ofs( s.b2 ))) );.J writeln('s.li: not(odd(ofs( s.li ))) );J end.

Procédures et fonctions

NEAR ouFAR 7 ...................................................50 Le cas le plus simple: une procédure sans paramètre ni variable locale .. .52 Variables locales ..................................................53 Transmission des paramètres ........................................56 Fonctions.........................................................63 Récursion.........................................................69

Les procédures et les fonctions sont les principales composantes des langages structurés: elles ne doivent donc pas être absentes de ce chapitre. Nous étudierons comment Turbo Pascal stocke et appelle les procédures et les fonctions, comment sont gérées les variables locales et les paramètres.

NEAR ou FAR?

A l'inverse de Turbo Pascal, le processeur ne fait pas de distinction entre procédures et fonctions. Pour lui il n'existe que des sous-programmes appelés procédures et parmi celles-ci il distingue le type FAR du type NEAR.

Cette différence ne se manifeste qu'au moment de la dernière instruction du sous-programme, c'est-à-dire lors du retour au programme appelant. L'instruction correspondante porte toujours le nom RET (Return), suffixé tantôt par N (NEAR), tantôt par F (FAR). Elle dépile l'adresse empilée précédemment par un ordre CALL et y poursuit l'exécution du programme.

L'instruction RETN se contente de prélever dans la pile une adresse de déplacement qui est aussitôt transférée dans le registre IP (compteur ordinal).

Le retour s'effectue donc vers un programme appelant qui se trouve dans le même segment de code que le sous-programme appelé.

L'instruction RETF permet par contre de revenir à un programme appelant qui se trouve dans un autre segment de code que le sous-programme appelé. En effet, RETF ne dépile pas seulement un déplacement mais aussi une adresse de segment qui est transférée dans le registre CS.

Aux deux instructions RET mentionnées correspondent deux instructions d'appel CALL. L'appel NEAR (NEAR Call) sert à appeler des sous-programmes qui se trouvent dans le même segment de code tandis que l'appel FAR (FAR Cail) sert à appeler des sous-programmes qui se trouvent dans d'autres segments de code. Les appels et les retours FAR consommant plus de temps machine que leurs homologues NEAR, un programme efficace s'appliquera à ne mettre en service des ordres FAR que dans la limite de l'indispensable.

Il est ainsi, bien sûr, de Turbo Pascal. Les concepteurs de la maison Borland ont développé un mécanisme raffiné d'attribution des modes NEAR et FAR.

  • les procédures et fonctions du programme principal sont essentiellement NEAR. car elles se trouvent toutes dans le même segment de code et s'appellent mutuellement.
  • les procédures et fonctions déclarées à l'intérieur d'une procédure ou d'une fonction sont également NEAR Etant appelées à un niveau local, elles peuvent être placées à l'intérieur du segment de code du programme appelant.
  • les procédures et fonctions qui sont mentionnées dans la partie Interface d'une unité sont FAR, car elles sont susceptibles d'être appelées à partir d'autres unités ou à partir du programme principal, c'est-à-dire à partir d'autres segments de code.
  • les procédures et fonctions qui ne sont mentionnées que dans la partie Implémentation d'une unité sont NEAR. On ne peut en effet y faire appel que de l'intérieur de l'unité, c'est-à-dire de l'intérieur du même segment de code.

Le type de la procédure ou de la fonction mise en service correspond en général au type d'appel NEAR ou CALL généré, sauf dans un cas particulier. Ce cas est celui où à l'intérieur d'une unité on fait appel à des routines mentionnées dans la partie Implémentation et de ce fait classifiées comme FAR. Ces routines devraient théoriquement être invoquées par un appel FAR car elles se terminent par une instruction RETF. Si on utilise un appel NEAR, le système se plante inévitablement au moment du retour car l'adresse de segment n'a pas été empilée. Mais plutôt que d'utiliser un appel FAR, Turbo Pascal préfère dans ce cas mettre en service une méthode plus efficace qui se traduit par les instructions:

push cs
call near routine

Cette séquence simule un appel FAR en empilant la valeur actuelle du segment de code avant de procéder à un appel NEAR standard. L'instruction de retour RETF qui clôture la routine appelée trouve alors sur la pile l'adresse qu'elle cherche. En même temps, quelques précieuses microsecondes sont gagnées par rapport à l'exécution d'un appel FAR complet.

Le programmeur peut introduire des exceptions aux règles générales précédemment exposées. Il lui suffit d'utiliser la directive de compilation {$F+) qui provoque la génération de procédures et de fonctions en mode FAR. Cette mesure n'a de sens que si vous appelez les procédures/ fonctions par des pointeurs, à moins que vous ne désiriez essayer quelques astuces de programmation, notamment en liaison avec l'assembleur. Vous trouverez des exemples de ce type dans les chapitres suivants. Le cas le plus simple : une procédure sans paramètre ni variable locale.

Nous allons examiner le cas le plus simple: une procédure sans paramètre ni variable locale. Le listing suivant montre comment une telle procédure est transformée en code machine et assembleur. program p_std;.J .1 ;Empile BP procedure test;.J .J ;Met la valeur de SP dans BP ).i begin.J ( cs:0000 55 push bp ( cs:0001 89E5 mov bp.sp J (-- Ici code machine correspondant à d'autres instructions Pascal - - -- J end; -J cs:0003 89EC mov sp,bp ;Reprend l'ancien SP ).J C cs:0005 50 pop bp :Dépile BP cs:0006 C3 ret ;Retour NEAR à l'appelant Li J 'J begi n.J test; ( A défaut de cet appel l'éditeur de liens élim4ne )_i la routine test Li cs:000F E8EEFF call TEST ;Appel NEAR à test.J end.

Vous voyez que la procédure test ci-dessus n'effectue aucune tâché. Elle se compose simplement d'un couple d'instructions Begin/End. On pourrait penser que le code généré se limite à une instruction RET. Mais que fait donc Turbo Pascal? Il fabrique quatre instructions supplémentaires qui manipulent de façon quelque peu obscure les registres BP et SP. En fait, ces instructions permettent de travailler avec les variables locales et d'accéder aux paramètres transmis. Elles réapparaîtront dans les exemples ultérieurs. Il est simplement dommage que Turbo Pascal les introduise alors même qu'elles sont inutiles.

Note: les quatre instructions ne consomment que 22 cycles machine, c'est-à-dire 2,6 microsecondes sur un AT à 8 MHz. Mais de telles inattentions réduisent à néant d'autres optimisations infiniment plus raffinées.

Variables locales

Partant de l'exemple précédent, nous avons introduit dans la procédure test trois variables locales. Leur format interne est identique à celui des variables globales mais l'endroit où elles sont mémorisées est différent. Examinons le listing suivant:

program p_varloc;.J J procedure test;.J -J var I integer;.J b : byte-,.j 5 : string-,.j -J begin.J t cs:0006 55 push bp ;Empile BP cs:0007 89E5 mov bp.sp ;Met la valeur de SP dans BP )j cs:0009 81EC0401 sub sp.0104 ;Réserve 259 octets pour les Li

variables Li

-J

- Salut' ;.J

cs:0000 BF0000 mov di .0000 ;Salut se trouve en CS:0000ILJ cs:0010 0E push cs ;Empile cette adresse commeLi cs:0011 57 push di ;pointeur FAR et charge le L-J cs:0012 8DBEFOFE lea di.[bp-0103] ;déplacement de S en DI ) cs:0016 16 push ss ;Combine DI avec SS comme cs:0017 57 push dl ;pointeur FAR et empile cs:0018 B8FFOO mov ax.00FF ;Longueur maxi - 255 octets ).j cs:OO1B 50 push ax ;à empiler comme paramètre Li cs:001C 9A21028F5F cail far StrCopy ;Copie Salut en S L-i -J

cs:0021 C746FEF1FF mov word ptr [bp-02].FFF1 :1 se trouve en ).J t ; [BP-2] L-J b :- 8:-J cs:0026 C646F008 mov byte ptr [bp-03].08 ;B se trouve en Li ( ; [BP-3] Li -J end ;J cs:002A 89EC mov sp.bp ;Recharge l'ancienne valeur de SP ).j efface les variables locales Li ( cs:002C 50 pop bp :Dépile BP cs:002D C3 ret ;Retour au programme appelant I-1 begin.J test; C Appel indispensable sinon l'éditeur de liens ).J t élimine la procédure end Au début de test, le contenu du registre BP est sauvegardé sur la pile. Dans l'exemple précédent cette disposition n'avait pas d'utilité: mais maintenant elle est importante en raison de l'ordre suivant qui modifie le contenu de BP. Le contenu du registre SP est mis dans BP, ce qui permet à BP de pointer au même endroit que SP. Mais les deux registres ne vont pas rester identiques très longtemps. Car l'instruction suivante soustrait déjà la valeur 260 à SP. Il s'agit là d'une étape décisive pour la gestion des variables locales, car la pile contient désormais un espace libre de 259 octets. C'est dans cet espace que Turbo Pascal va mémoriser les variables locales de la procédure. On pourrait pensera priori que la pile n'est pas un endroit de tout repos pour stocker des variables. Mais après mûre réflexion, il apparaît au contraire qu'il s'agit là d'un endroit tout à fait idéal.

D'une manière générale, les informations stockées sur la pile n'y restent pas longtemps c'est ce que nous a montré par exemple la gestion de l'adresse de retour associée à une instruction CALL. Chaque fois qu'un sous-programme rend la main au programme appelant, l'information constituée par l'adresse de retour devient inaccessible. Elle est même écrasée dès qu'on empile une nouvelle donnée. La pile fonctionne donc selon une certaine dynamique, et c'est cette dynamique qui précisément la prédestine au stockage des variables locales.

Car les variables locales - tout comme les adresses de retour - n'ont qu'une durée de vie limitée à l'exécution de la fonction ou de la procédure. Il serait évidemment possible de disposer les variables locales dans le segment de données mais ce segment se révélerait vite trop petit pour l'ensemble des variables généralement utilisées dans un programme. Il faudrait en fait choisir un espace de recouvrement uniquement réservé aux variables locales, qui ne serait occupé à un moment donné que par celles qui appartiennent à la fonction ou la procédure en cours d'exécution. Cet espace serait juste assez vaste pour contenir les variables locales de la fonction ou de la procédure qui en met en oeuvre la plus grande quantité. Mais ce système ne fonctionnerait pas car les procédures et les fonctions peuvent s'appeler réciproquement dans l'espace de recouvrement les variables de l'une écraseraient les variables de l'autre!

En plaçant le pointeur de pile au-delà de la zone de ses variables locales, la procédure test met cette zone à l'abri des stockages qui pourraient en écraser les données. Ainsi en cas d'appel à un sous-programme -qui empile l'adresse de retour et introduit ses propres variables locales- la zone reste intacte. Chaque fois qu'une procédure ou une fonction est appelée, elle déplace le pointeur de pile derrière ses variables locales pour les protéger contre toute agression ennemie. Le pointeur de pile descend ainsi vers le début de la pile en mémoire centrale, s'éloignant des variables locales préservées.

Bien entendu, à la fin de l'exécution d'une procédure ou d'une fonction, le pointeur de pile doit se retrouver dans la position qu'il avait au début de l'exécution. Les instructions appropriées sont mises en place par Turbo Pascal. C'est d'abord le registre SP qui est chargé avec le contenu de BP. II retrouve ainsi sa valeur d'origine et se déplace par-dessus les variables locales vers la fin de la pile. II pointe à nouveau sur l'endroit où le contenu original du registre BP avait été sauvegardé par le PUSH d'introduction. Ce contenu est ensuite repris par l'instruction POP et le registre BP retrouve lui aussi sa valeur d'origine.

Le registre SP pointe maintenant sur l'adresse de retour au programme appelant: l'instruction RET clôture le sous-programme et confie la poursuite de l'exécution au programme appelant. On pourrait penser que seul le registre SP sert à gérer les variables locales: à quoi bon alors manipuler le registre BP ? En fait, le processeur a prévu le registre BP pour l'adressage indexé de la pile: quand on utilise ce registre, c'est le registre SS -au lieu de DS- qui intervient pour former l'adresse complète. Il en résulte que BP est indispensable pour adresser les variables locales à l'aide de la pile.

Les instructions disposées entre le début et la fin de la procédure illustrent clairement ce mécanisme. Si on examine le code machine généré, on constate que I commence à l'adresse [BP-$2], B en [BP-$31 et la chaîne S en [BP-$103]. Ces adresses n'ont pas été choisies au hasard, mais elles découlent de la taille respective des variables et de l'ordre dans lequel ces variables sont déclarées dans la procédure.

C'est la variable I qui est créée en premier: elle prend 2 octets. A l'adresse [BP-01 se trouve encore l'octet inférieur de l'ancien contenu de BP: il ne faut donc pas que les variables locales dépassent l'adresse [BP-1]. Pour cette raison I commence en IBP-21 et se termine exactement en [BP-11. La variable B est disposée juste après I. Comme elle n'occupe qu'un octet, elle est rangée sur la pile sous forme de BYTE en [BP-3]. La dernière variable locale est la chaîne S. Elle occupe 256 octets et doit donc commencer 256 octets derrière la variable B, ce qui correspond à l'adresse [BP-259].

Chaque procédure ou fonction se comportant de la façon décrite plus haut, il est possible d'imbriquer les appels pratiquement sans limite, aucune procédure ou fonction ne pouvant empiéter sur la sphère privée de l'autre.

Turbo Pascal n'est pas le seul langage à exploiter la pile pour gérer les variables locales. Tous les langages évolués se servent de cette technique qui est d'ailleurs elle-même soutenue par les instructions et l'architecture du processeur, par le moyen de l'adressage indexé selon BP.

Transmission des paramètres

Les paramètres des procédures et des fonctions sont également transmis par l'intermédiaire de la pile qui reçoit une à une les valeurs correspondantes. La transmission se fait de gauche à droite, de sorte que le déplacement par rapport au pointeur de pile augmente également de gauche à droite. Tout comme les variables locales, les paramètres transmis sont adressés par le registre Bi'. Mais contrairement aux variables locales, ils ont un déplacement positif. Examinez à cet effet le programme suivant:

program p_para;J J var gi : integer;..J

procedure test( I integer; var j : integer );.J J begi n.J cs:0000 55 push bp ;Initialise la plie Li ( cs:0001 89E5 mov bp,sp J j cs:0003 8B4608 mov ax.[bp+08] ;Prend le paramètre I cs:0006 C47E04 les di.[bp+04] ;Charge en ES:DI le pointeur )J ;transmis pour J Li cs:0009 268905 mov es:[di].ax ;et y copie la valeur de I Li J end ;.J cs:000C 89EC mov sp.bp ;Restaure la pile [ cs:000E 5D pop bp ; Li cs:000F C20600 ret 0006 ;Retour au programme appelant Li ;retlre les arguments de la Li E ;pile J begin.J test( 5. gi );.J cs:001A B80500 mov ax.0005 :Donne à I la valeur 5 Li cs:0010 50 push ax ;et l'empile comme paramètre)J cs:001E BF3EO0 mov dl.offset gi ;Transmet l'adresse de G! )J cs:0021 lE push ds ;comme pointeur FAR en le )J cs:0022 57 push dl ;mettant sur la pile Li cs:0023 E8DAFF cali TEST J end.

Dans cet exemple, deux paramètres de type Integer sont transmis à la procédure test. Le premier paramètre est transmis sous forme de valeur, et le second sous forme de variable, ce qui constitue une différence essentielle comme le montrent les commentaires du programme.

Etudions d'abord l'appel de test à l'intérieur du programme principal. Le paramètre I est d'abord mis sur la pile avec la valeur 5. Concrètement, la valeur 5 est chargée dans AX et le registre AX est empilé. Ce mode de fonctionnement consiste à donner à test une copie locale du paramètre, une variable qui reflète sa valeur mais qui ne se confond pas avec lui. Les choses se passent différemment avec le deuxième paramètre pour lequel on mobilise la variable CI. Comme la procédure test doit traiter ce paramètre comme un paramètre variable, elle doit avoir la possibilité de le modifier durablement. Il ne suffit plus à Turbo Pascal de copier le contenu du paramètre sur la pile : il doit transmettre l'adresse où se trouve la variable pour que la procédure puisse véritablement y accéder.

Même si les instructions Pascal ne le laissent pas voir directement, tous les accès effectués sur des paramètres variables à l'intérieur d'une procédure ou d'une fonction sont des opérations sur des pointeurs. Pour chaque paramètre variable, Turbo Pascal met en service un pointeur qui est -comme d'habitude dans ce langage- un pointeur de type FAR. La pile reçoit d'abord l'adresse du segment, et ensuite l'adresse du déplacement. Comme à chaque instruction PUSH le pointeur de pile se rapproche du début de segment de pile en mémoire centrale, l'adresse du déplacement précède finalement l'adresse du segment, exactement comme l'exige la convention Intel.

La procédure Test initialise d'abord la pile de façon usuelle: elle peut alors accéder aux paramètres transmis au moyen du registre BP. Pour charger J avec la valeur de I, elle place d'abord le contenu de I dans AX. Pour pouvoir manipuler la variable provenant du paramètre J, elle doit en prendre l'adresse dans la pile. Cette opération est réalisée à l'aide de l'instruction LES que Turbo Pascal utilise toujours pour accéder aux variables référencées par des pointeurs. L'instruction en question charge un pointeur FAR situé à l'endroit indiqué, l'adresse du segment étant transférée dans ES et le déplacement dans le registre qui forme le premier opérande.

Ce registre doit obligatoirement être l'un des registres SI, DI ou BX.

Le processeur peut aussi charger le déplacement dans l'un des registres généraux, mais ceux-ci ne pouvant pas être utilisés pour l'adressage indexé ne sont pas qualifiés pour référencer des pointeurs.

Turbo Pascal utilise généralement DI. Le registre de segment ne peut être que ES, car c'est ici le seul registre qui n'est pas astreint à pointer constamment sur le même segment. Dénommé "Extra Segment", il est justement réservé à ce type de tâche. Une fois l'adresse ainsi chargée, la procédure test est en mesure d'accéder à la variable transmise CI.

Quand la procédure est terminée, la pile est soumise à l'opération de nettoyage habituelle. Notez bien que l'instruction RET est utilisée ici sous une forme particulière, en association avec un nombre. Ce nombre représente le nombre d'octets ajoutés au contenu du pointeur de pile pour le ramener vers la fin de la pile. En même temps qu'elle communique l'adresse de retour, l'instruction RET ainsi exploitée permet d'éliminer les paramètres de la pile.

L'exemple qui vient d'être donné montre comment Turbo Pascal transmet les paramètres par valeur lorsqu'ils ne sont pas destinés à être modifiés, et par adresse lorsqu'ils sont variables. Le type des paramètres n'influence pas leur transmission lorsqu'il s'agit de paramètres variables: dans ce cas la transmission s'effectue toujours sous forme de pointeur.

Mais il n'en est pas de même lorsque les paramètres sont transmis par valeur. La transmission dépend alors du type du paramètre, comme le montre l'exemple suivant:

program p_paral;J 'J type str8 - string[8];.J 'J var gs8 str8;J gs255 string;.J -J procedure test( b : byte; 1 : longint; p : pointer;.J r : real; s8: str8; s255 string );.J 'J begi n.J cs:0000 55 push bp ;Initialise la pile Li { cs:0001 89E5 mov bp,sp cs:0003 81ECOA01 sub sp,010A ;Réserve 266 octets sur ;la pile Li cs:0007 C47E08 les dl.[bp+08] ;Cherche dans la pile un Li cs:000A 06 push es ;pointeur qui référence S8 Li cs:000B 57 push dl ;et l'empile comme argument ).i ( ; de StrCopy cs:000C 8D7EF7 lea dl.[bp-09] ;Empile l'adresse de la copie Li f cs:000F 16 push ss ;locale de S8 C SS:[BP-9] ) Li f cs:0010 57 push dl ; cs;0011 B80800 mov ax,0008 ;Copie 8 octets au maximum Li f cs:0014 50 push ax f cs:0015 9A2102975F call StrCopy f cs:001A C47E04 les di.[bp+04] ;Cherche dans la pile un Li f cs:OO1D 06 push es ;pointeur sur S255 et l'empileLJ cs:001E 57 push di ;comme argument de StrCopy ).i f cs:001F 8DBEF7FE lea di.[bp-0109] ;La copie locale de S255 L-i f cs:0023 16 push ss ;commence en SS:[BP-$109] Li f cs:0024 57 push di L-i cs:0025 B8FFOO mov ax.00FF ;Copie 255 octets au maximum Li f cs0028 50 push ax Li f cs:0029 9A2102975F call StrCopy Li 'J Les Instructions Pascal mises ici peuvent accéder aux variables transmises ) .i J end ;.J f cs:002E 89EC mov sp.bp f cs:0030 50 pop bp cs;0031 C21800 ret 0018 J begi n.J test( 1. $12345678. fil, f cs:003C BOOl f cs:003E 50 f cs:003F B87856 f cs:0042 BA3412 f cs:0045 52 f cs:0046 50 f cs:0047 31C0 cs:0049 3102 f cs:004B 52 f cs:004C 50 cs:0040 888221 f cs:0050 BBA2DA f cs:0053 BÀOF49 f cs:0056 52 f cs:0057 53 C cs:0058 50 ( cs:0059 BF3EO0 f cs:005C lE f cs:0050 57 ( cs:OOSE BF4700 C cs:0061 lE f cs:0062 57 f cs:0063 E89AFF end.

Restaure la pile Li
Retire les arguments Li

pi, gs8. gs255 );.J mov al,01 ;Les arguments qui sont des )_i octets sont compactés en ).i push ax ; WORD avant transsmission Li mov ax,5678 :Charge le paramètre L mov dx,1234 ;en DX:AX push dx ;et le met sur la pile push ax ; xcr ax.ax ;Argument - pointeur NIL ).i xor dx.dx ;Charge le paramètre P U push dx ;en DX:AX et le met Li push ax ; sur la pile Li mov ax,2182 ;Charge le nombre réel Pi )_i mov bx.DAA2 ; qui prend 6 octets L-J mov dx,490F ; en DX:BX:AX push dx ; et le place sur la pile Li push bx Li push ax Li mov di,offset gs8;Empile un pointeur sur ).J push ds ; GS8 L-i push di U mov di,offset gs255 ;Empile un pointeur push ds ; sur GS255 push dl U call TEST

Pour les besoins de notre démonstration, nous avons transmis 6 différents paramètres à la procédure: un octet, un entier long, un pointeur, un réel, une chaîne de longueur 8 et une chaîne standard susceptible de contenir jusqu'à 255 caractères. Dans l'exemple précédent, le type entier Integer correspondait à une gestion interne par WORD. De même ici, le type Byte (octet) correspond à tous les types gérés de façon interne par BYTE. II s'agit non seulement des types Char, Shortlnt et Boolean mais aussi du type énuméré et du type intervalle qui sont susceptibles de tenir sur un octet. Les entiers longs et les pointeurs -qu'ils soient typés ou non- sont transmis de façon qu'ils se retrouvent dans la pile selon leur format normal. Avec les entiers longs (Longlnt) on empile d'abord le mot de poids fort puis le mot de poids faible. Avec les pointeurs on empile d'abord l'adresse du segment puis le déplacement. Les nombres en virgule flottante de type Real sont également passés sur la pile, avec préservation de leur format standard sur 6 octets. L'empilage s'effectue successivement sur le mot de poids fort, le mot de poids intermédiaire et le mot de poids faible.

La transmission des chaînes de caractères révèle une surprise de taille: le programme appelant transmet simplement un pointeur et la procédure commence par copier la chaîne dans une variable locale créée spécialement à cet effet, sansque le programmeur l'ait demandé. La place réservée est juste suffisante pour le but recherché :9 octets pour la variable S8, à partir de l'adresse [BP-$9] et 256 octets pour la variable S255, à partir de l'adresse [BP-$109].

La copie est prise en charge par une procédure interne de l'unité System appelée StrCopy, et que nous retrouverons souvent par la suite. Bien que son examen révèle qu'elle a été écrite en assembleur et non pas en Pascal, elle reçoit ses paramètres à la façon de Pascal au moyen de la pile. Ces paramètres sont au nombre de trois: le premier est un pointeur sur la chaîne source, le deuxième est aussi un pointeur qui pointe sur le tampon dédié à la copie locale, le troisième transmet la longueur maximale de la chaîne. Avant que la routine StrCopy ne duplique la chaîne source, elle teste sa longueur de façon à ne pas entreprendre de travail inutile et à ne copier que le strict nécessaire. Voici le listing de cette procédure en assembleur:

py proc far.J

Pour compter en incrémentation J

bx.sp ;Charge BX avec le pointeur de plle.J dx.ds ;Sauvegarder OS en DX.J si,ss:[bx+OA] ;Pointeur sur chaîne source en DS:SI.J di.ss:[bx+06] ;Pointeur sur copie locale en ES:DI..J cx.ss:[bx+04] ;Longueur maximale en CXJ

Charge la longueur de la chaîne source en AU

al .cl ;Compare avec la longueur maximale J kg ;Plus petit ou égal ? Oui ---> ppeJ al .cl ;Initialise la compteur AL avec la J longueur maximale de la chaîne J ;Mémorise l'octet qui donne la longueurJ dans la copie localeJ

Charge dans CL la longueur à copierJ ;Met à O le poids fort du compteurJ ;Copie CX octets de DS
SI en ES:DIJ ;Restaure OS J
Retour à l'appelant avec nettoyage J de la pileJ

cld mov mov 1 ds les mov 1 odsb cmp j be mov ppe: stosb mov cl.al xor ch.ch rep movsb mov ds.dx ret 000A St rC

Nous venons d'examiner la transmission des paramètres standard. Mais comment se passe la transmission des types composés tels que tableaux, enregistrements ou ensembles? En fait, malgré leur diversité, Turbo Pascal traite ces types selon une règle unique que nous allons étudier à travers un exemple. Le programme suivant met en oeuvre 6 types de tableaux différents, ayant respectivement 1, 2, 3 4 5 et 6 octets. La mémoire occupée est donc en correspondance directe avec le nombre des éléments, et nous allons voir que c'est précisément cette quantité de mémoire qui va influencer le comportement de Turbo Pascal.

program p_para2:.J .J type arrayl - array [1. .1] of byte;.J array2 - array [1-2] 0f byte;' array3 - array [1-3] 0f byte;.J array4 - array [1. .4] 0f byte-.-j array5 - array [1. .5] 0f byte-.-j array6 - array [1. .6] 0f byte;.J -J var gal arrayl; ga2 : array2; ga3 : array3;.i ga4 : array4; ga5 : array5; ga6 : array6;J -J procedure test( al arrayl; a2 array2; a3 : array3;J a4 : array4; a5 : array5; a6 : array6 );.J --J begi n_i cs:0000 55 push bp ;Initialise la pile Li f cs:0001 89E5 mov bp.sp cs:0003 83ECOE sub sp.000E ;Réserve de la place pour les copies locales Li cs:0006 C47E10 les di,[bp+10] ;Chercher dans la pile un cs:0009 06 push es ; pointeur sur A3 et le mettre ).i cs:000A 57 push di ; sur la pile cs:000B 8D7EFD lea di.[bp-03] ;La copie locale de A3 commence Li f cs:000E 16 push ss ; à l'adresse [BP- 31 de la pileLi cs:000F 57 push di ;Dispose cette adresse sur la ).i pile cs:0010 B80300 mov ax.0003 ;A3 occupe 3 octets cs;0013 50 push ax ;Met ce nombre sur la pile Li cs:0014 9A0702985F call MemCopy ;Déclenche une copie locale Li cs:0019 C47E08 les di,Ebp+081 ;Chercher dans la pile un Li cs:001C 06 push es pointeur sur AS et le mettre L cs:001D 57 push di ; sur la pile ).i cs:OO1E 8D7EF8 lea di.[bp-08] ;La copie locale de A5 commence l_i cs:0021 16 push ss ; à l'adresse [BP-8] Li f cs:0022 57 push di cs:0023 B80500 mov ax.0005 ;A5 occupe 5 octets f cs:0026 50 push ax cs:0027 9A0702985F call MemCopy ;Fabrique une copie locale Li cs:002C C47E04 les di.[bp+04] ;Idem avec A6 f cs:002F 06 push es ; f cs:0030 57 push di 1-1 f cs:0031 807EF2 lea dl,[bp-OE] f cs:0034 16 push ss Li f cs:0035 57 push di cs:0036 880600 mov ax.0006 f cs:0039 50 push ax l_i f cs:003A 9A0702985F call MemCopy --J f Les instructions Pascal mises Ici peuvent accéder aux tableaux f transmis ) .i end ;_i cs:003F 89EC mov sp.bp ;Restaure la pile f cs:0041 50 pop bp Li ( cs0042 C21400 ret 0014 ;Enlève les paramètres de la )_i f ; pile Li

begin.J test( gal. ga2. ga3, ga4. ga5. ga6 );.I cs:004D A03EO0 mov al.[GA1] ;Empile GAl sous forme d'octet )J cs:0050 50 push ax ; transformé en WORD ( cs:0051 FF363F00 push [GA2] ;Empile GA2 cs:0055 BF4100 mov di.offset ga3 ;Empile un pointeur sur C cs:0058 lE push ds ;GA3 C cs:0059 57 push dl cs:005A FF364600 push [GA4+2] ;Empile GA4 sous forme de DWORD J.J C cs:005E FF364400 push [GA4] C cs:0062 BF4800 mov di,offset GA5 ;Empile GA5 C cs:0065 lE push ds C cs:0066 57 push di cs:0067 BF4000 mov dl.offset GA6 ;Empile un pointeur sur 11—i C cs:006A lE push ds GA6 L-i E cs:006B 57 push di L-i cs:006C E891FF cail TEST L-i end.

La procédure test recueille six variables transmises par valeur, chacune d'elles correspondant à l'un des 6 types de tableaux. Les tableaux se présentent par ordre croissant de taille. Le premier paramètre est un tableau à un octet le deuxième un tableau à deux octets, et ainsi de suite. Notre attention va se focaliser sur la manière dont les paramètres sont mis sur la pile au moment où le programme principal appelle la procédure test. Lepremier tableau qui ne comprend qu'un seul élément d'un octet est empilé comme un =format format interne WORD. La même opération est appliquée au deuxième tableau qui comprend deux éléments d'un octet. Ici il n'est pas nécessaire de transformer un octet en un mot de format WORD, le tableau est transféré directement de la mémoire à la pile. Le troisième tableau (à 3 octets) n'est pas disposé lui-même sur la pile: à sa place c'est un pointeur qui est empilé, comme dans le cas de la transmission d'un paramètre variable. Pour égarer le chercheur, le tableau suivant (à 4 octets) est à nouveau directement empilé. C'est d'abord le mot de poids fort qui est transmis, suivi du mot de poids faible: l'arrivée des deux formats WORD sur la pile reconstitue l'image du tableau en mémoire. Les deux tableaux suivants sont à nouveau transmis sous forme de pointeurs. Et si on continue la série, on s'aperçoit que Turbo Pascal utilise systématiquement des pointeurs pour transmettre des types composés de plus de 5 octets. Lorsque Turbo Pascal prend l'initiative de transmettre certains paramètres comme paramètres variables, la procédure appelée doit en tenir compte et créer des copies locales des paramètres concernés. Tout comme les chaînes donnaient lieu à des copies locales, les types composés sont traités comme des variables locales additionnelles. A la place de la procédure StrCopy, c'est une autre procédure interne appelée MemCopy et tirée elle aussi de l'unité System qui entre en jeu. Elle copie un nombre donné d'octets prélevés sur une variable source et transférés dans le tampon dédié à la copie locale. Voici pour conclure une récapitulation des différentes méthodes de transmission des paramètres qui ont été décrites dans ce chapitre. 2.62 La Bible Turbo Pascal 2-63 Dans les coulisses du langage Paramètres variables typés ou non POINTER (DWORD) Char BYTE dans WORD Boolean BYTE dans WORD Byte BYTE dans WORD Shortlnt BYTE dans WORD Integer WORD Word WORD Longlnt DWORD Pointeurs typés ou non POINTER (DWORD) Real 3WORD Strings POINTER (DWORD) Types énumérés et Intervalle BYTE dans WORD ou WORD Types composés de longueur: 1 octet BYTE dans WORD 2 octets WORD 3 octets POINTER (DWORD) 4 octets DWORD >4 octets POINTER (DWORD) Figure 20: Format des paramètres transmis à une procédure/fonction par la pile V Fonctions Du point de vue de la gestion des variables locales et de la transmission des données, les fonctions ne se distinguent pas des procédures. Mais elles doivent en plus gérer un résultat et le renvoyer au programme appelant. Le listing suivant montre comment Turbo Pascal se débrouille pour remplir ces obligations: program f_std;.J J var I integer;.J J function test : integer;J J beginJ cs:0000 55 push bp ;Sauvegarde BP sur la pile cs:0001 89E5 mov bp.sp ;Transfère le contenu de SP dans BP cs:0003 83ECO2 sub sp,0002 ;Réserve de la place dans la pile)J pour le résultat J test :- $4711;.J cs:0006 C746FE1147 mov word ptr [bp-02].$4711 ;Charge le résultat ).J de la fonction ). -J end cs:00013 8B46FE mov ax,[bp-02] ;Transfère le résultat en AX )_i cs:000E 89EC mov sp.bp :Rend à SP son ancienne valeur ).J ( cs:0010 50 pop bp ;Restaure BP ).-i cs:0011 C3 ret ;Retour au programme appelant )..J .J .J begin.J I test;-1 cs:001A E8E3FF call TEST ;Appelle la fonction TEST ).J cs:0010 A33E00 mov [I].ax ;Sauvegarde le résultat end. Dans les coulisses du langage 2-63 La Bible Turbo Pascal 2-64 La fonction test retourne un résultat entier de type Integer. Comme Pascal ne prescrit pas le moment où le résultat doit être affecté, une variable locale est créée pour recueillir ce résultat. Le processus se déroule selon le mécanisme habituel en retranchant du pointeur de pile le nombre d'octets à réserver. L'affectation du résultat de la fonction est traitée comme n'importe quelle affectation de variable locale. Le résultat de la fonction n'est pas transmis par la pile mais par les registres du processeur où le programme appelant peut le lire directement. A la fin de la fonction test, Turbo Pascal range le résultat de type Integer dans le registre AX. Le transfert dans A)( se fait juste avant le rétablissement de la pile initiale, qui entraîne la perte des variables locales. Pour transmettre le résultat, Turbo Pascal met en service différents registres selon les circonstances. Le listing suivant montre quelques exemples: program f_regs;.J J var b : byte;.l 1 : longint;.J r real;J p : pointer;.J

function fbyte : byte;J

begi nJ f cs:0000 55 push f cs:0001 89E5 mov ( cs:0003 83ECO2 sub J fbyte :- 255;J f cs:0006 C646FFFF mov .J end;.J f cs:000A 8A46FF mov C cs:0000 89EC mov C cs:000F 50 pop C cs:0010 C3 ret J function flongint : longint;J

beginJ f cs:0011 55 push f cs:0012 89E5 mov f cs:0014 83EC04 sub

flongint $12345678;.J f cs:0017 C746FC7856 mov f cs:001C C746FE3412 mov bp ;Initialise la pile bp.sp sp,0002 ;2 octets pour le résultat 3J byte ptr [bp-01].FF ;Fixe le résultat 3J al.[bp-01] ;Transfère le résultat en AL 3.J sp.bp ;Restaure la pile 3.-i bp 3.-i

Retour au programme appelant ).J

bp ;Initialise la pile )J bp.sp 3_l sp.0004 ;Réserve 4 octets pour le type 3.i Longlnt 3_l word ptr [bp-04].5678 ;Fixe le résultat 3_i word ptr [bp-02].1234 3_i end -..j cs:0021 C446FC les ax,[bp-04] ;Met le résultat dans ES:AX ).J cs:0024 8CC2 mov dx.es ;Le transfère dans DX:AX 3.-i cs:0026 89EC mov sp.bp ;Restaure la pile 3_J f cs:0028 50 pop bp 3_i cs:0029 C3 ret ;Retour au programme appelant 3.-1 J function freal : real;J J beginJ f cs:002A 55 push bp ;..Comme d'habitude 3_J f cs:002B 89E5 mov bp.sp 3_i f cs:002D 83EC06 sub sp.0006 ;Un réel prend 6 octets 3_i J freal Pi;J 2-64 La Bible Turbo Pascal 2-65 Dans les coulisses du langage C cs:0030 C746FA8221 mov word ptr [bp-06],2182 ;Fixe le résultat Li - C cs:0035 C746FCA2DA mov word ptr [bp-04].DAA2 C cs:003A C746FE0F49 mov word ptr [bp-02].490F ).i end ;.J cs:003F 8B46FA mov ax.[bp-06] ;Les réels sont transmis par lesLJ C cs:0042 8B5EFC mov bx.[bp-04] :registres DX:BX:AX ).J C cs:0045 8B56FE mov dx,[bp-02] C cs:0048 89EC mov sp.bp Li C cs:004A 5D pop bp C cs:004B C3 ret Li -J function fpointer : pointer-,-J -J begi ni C cs:004C 55 push bp :Initialise la pile Li C cs:004D 89E5 mov bp.sp C cs:004F 83EC04 sub sp.0004 ;Réserve 4 octets pour un pointeurLi J fpointer :- NIL;.J C cs:0052 31C0 xor ax.ax ;NIL - Segmentt Déplacement Li cs:0054 8946FC mov [bp-04],ax nuls Li C cs:0057 8946FE mov [bp-02],ax Li -J end; C cs:005A C446FC les ax.[bp-04] ;Mécanisme de retour du résultatLi C identique à celui des Longlnt Li C cs:005D 8CC2 mov dx.es ;Utilise les registres DX:AX ).J C cs:005F 89EC mov sp,bp Li C cs:0061 50 pop bp C cs:0062 C3 ret ;Terminé

-J begin b :- fbyte;J cs:006B E892FF call FBYTE ;Appelle la fonction FBYTE ).J C cs:006E A23E00 mov [B],al ;Mémorise le résultat .i 1 :- flongint;.J C cs:0071 E89DFF call FLONGINT ;Appelle la fonction FLONGINT Li C cs:0074 A33F00 mov [L].ax ;Le résultat est retourné Li C cs:0077 89164100 mov [L+2].dx dans DX:AX Li -J r :- freal;.i C cs:007B E8ACFF call FREAL ;Appelle la fonction FREAL )J C cs:007E A34300 mov [R].ax :mot inférieur en AX C cs:0081 891E4500 mov [R+2),bx ;mot intermédiaire en BX Li C cs:0085 89164700 mov [R+4].dx ;mot supérieur en BX Li -J p :- fpointer-..j cs:0089 E8COFF call FPOINTER ;Appelle la fonction FPOINTER Li C cs:008C A34900 mov [P].ax ;Retourne le pointeur Li cs:008F 89164800 mov [P+2].dx dans DX:AX Li end. Pour illustrer les mécanismes de retour des valeurs, quatre fonctions ont été inventées qui renvoient respectivement des résultats de type Byte, Longlnt, Real et Pointer. Ces fonctions représentent aussi les autres types qui ont le même format interne. La forme des appels issus du programme principal suffit déjà à montrer comment les valeurs des fonctions sont retournées: s'il s'agit d'un octet (correspondant aux types Char, Boolean, Shortint, énuméré et intervalle) il est transmis par le demi-registre AL. S'il s'agit d'un entier long (Longlnt), il est transmis par la paire de registres DX:AX contenant respectivement le mot de poids fort (DX) et le mot de poids faible (AX). Ces Dans les coulisses du langage 2-65 La Bible Turbo Pascal 2-66 deux registres sont également mis en service pour retourner des pointeurs, typés ou non: dans ce cas AX contient le déplacement et DX le segment. Les six octets nécessités par le type Real sont retournés dans les registres AX, BX et DX. DX reçoit le mot le plus significatif, c'est-à-dire les deux octets de poids supérieur. Les deux octets intermédiaires sont recueillis par le registre BX, et les deux octets les moins significatifs par le registre A)(. Les types composés, du genre tableaux ou enregistrements, ne peuvent pas constituer des résultats de fonction en langage Pascal. On ne se posera donc pas de problème à leur sujet. Mais il n'en est pas de même des chaînes de caractères qui peuvent être retournées par les fonctions. Le mécanisme de ce retour est illustré par l'exemple suivant: program f_str;.J J type CheminDos - string[64];J J var NouvoChemin : CheminDos;.J s : Striflg;.J

function fChemin : CheminDos;J J begin.J f cs:0007 55 push bp ;Comme d'habitude... cs:0008 89E5 mov bp,sp Attentioni Aucune place mémoire n'est réservée pour recueillir le résultat de la fonction car le programme appelant doit mettre sur la pile un pointeur qui pointe sur un tampon .J fChemin :— 'c:\dos' ;.J f cs:000A BF0000 f cs:0000 0E f cs:000E 57 f cs:000F C47E04 f cs:0012 06 f cs:0013 57 f cs:0014 B84000 f cs:0017 50 ( cs:0018 9A2102995F mov di .0000 ;La constante chaîne 'c:dos' )J push cs se trouve en CS:0000, adresse).J push di qui est empilée les di.[bp-+-04] ;Mettre l'adresse transmise Li push es en ES:DI et la disposer sur)J push di la pile mov ax.0040 ;La longueur maxi pour push ax ; StrCopy est de 64 octets )J call far StrCopy J end ;.J E cs:001D 89EC mov E cs:001F 50 POP ( cs:0020 C3 ret function fChaine: string;J J begin.I f cs:0031 55 push cs:0032 89E5 mov J sp.bp ;Rétablit la pile bp

Le pointeur transmis reste )J

sur la pile bp ;Même principe : Turbo Pascal Li bp.sp ; copie la constante chaîne ).J fChaine :— 'Fonction-Chaîne' ;J cs:0034 SF2100 mov E cs:0037 0E push E cs:0038 57 push f cs:0039 C47E04 les f cs:003C 06 push f cs:0030 57 push f cs:003E B8FFOO mov f cs:0041 50 push f cs:0042 9A2102995F cail end;J f cs:0047 89EC mov di .0021 située en CS:0021 en se )J cs ; servant de la routine di StrCopy. Li di.[bp+04] ;La copie est disposée dans )J es le tampon du programme )J di appelant. Li ax.00FF ;Seule différence: la long. Li ax maximale pour StrCopy est )J far StrCopy maintenant de 255 octets ).i .bp ;Débarrasse la pile Li 2-66 La Bible Turbo Pascal 2-67 Dans les coulisses du langage C cs:0049 50 pop bp C cs:004A C3 ret Li 'j begin.J cs:0050 55 push bp ;Le programme principal se ).j cs:0051 89ES mov bp.sp ; comporte comme une procédure Li cs:0053 81EC0001 sub sp.0100 ;Réserve 256 octets sur la pile) pour la copie de la chaîne )..i '-J NouvoChemin fChemin;J cs:0057 8DBEOOFF lea di.[bp-0100];Charge le déplacement du ).-i cs:OOSB 16 push ss tampon pour l'empiler avec Li cs:OOSC 57 push di le segment SS comme Li pointeur FAR cs:005D E8A7FF call FCHEMIN appelle FCHEMIN ( cs:0060 8F3E00 mov di.offset NouvoChemin Li cs:0063 lE push ds ;Un pointeur FAR sur cs:0064 57 push di NouvoChemin est empilé Li cs:0065 884000 mov ax.0040 ;Long. maximale - 64 octets ),j C cs:0068 50 push ax ( cs:0069 9A2102995F call far StrCopy ;Invoque StrCopy L-i '-J s : fChaine;.J cs:006E 8DBEOOFF lea di,[bp-0100];Hême mécanisme, mais avec L-i cs:0072 16 push ss ; d'autres paramètres C cs:0073 57 push di Li C cs:0074 E8BAFF call FCHAINE cs:0077 BF7FOO mov di,007F ;Met l'adresse de S sur la ),i cs:007A lE push ds pile L-i C cs:0078 57 push di cs:007C B8FFOO mov ax,OOFF ;Copier 255 octets maxim. Li C cs:007F 50 push ax ( cs:0080 9A2102995F call far StrCopy ;StrCopy Li J end..i cs:0085 89EC mov sp.bp ;Retire de la pile le tampon Li cs:0087 5D pop bp ; prévu pour la chaîne Ce programme montre deux exemples de chaînes renvoyées comme résultats de fonction. L'une des chaînes est de type standard, avec une longueur maximale de 255 octets. L'autre chaîne est du type CheminDos, prévu pour 64 caractères au maximum. La fonction fChemin renvoie un résultat de type CheminDos, tandis que la fonction fChaine renvoie un résultat de type chaîne standard. L'une et l'autre fonction sont appelées par le programme principal. Les résultats sont affectés tantôt à la variable NouvoChemin, tantôt à la variable s. En examinant le début du programme principal, on s'aperçoit tout de suite qu'il initialise la pile à la manière d'une procédure ou d'une fonction: le registre BP est empilé, après quoi le contenu du pointeur de pile est chargé dans BP. Ce processus est normal, car pour Turbo Pascal un programme principal n'est rien qu'autre qu'une procédure démarrée automatiquement, sans appel explicite, au moment du lancement. Mais le listing révèle aussi qu'avant d'exécuter la première instruction, Turbo Pascal réduit le pointeur de pile de 256 octets, œ qui ménage de la place pour des variables locales. Des variables locales dans un programme principal, voilà qui est plutôt étrange! Mais le compilateur a besoin de ces 256 octets pour affecter les résultats des fonctions fChemin et fChaine aux variables NouvoChemin et s. Le fonctionnement de ce mécanisme est révélé par la compilation de l'instruction NouvoChemin:— fChemin; I Dans les coulisses du langage 2-67 La Bible Turbo Pascal 2.68 Bien que la fonction fChemin n'attende aucun paramètre, Turbo Pascal en charge un sur la pile juste avant d'y faire appel. Le paramètre en question est un pointeur qui pointe sur le début du tampon de 256 octets précédemment réservé sur la pile. Quand la fonction fChemin intervient, elle initialise la pile de la manière habituelle, mais elle ne réserve aucune place pour une variable locale destinée à recueillir le résultat qu'elle retournera (ici donc une chaîne de caractères). L'accès au résultat se fait par le pointeur transmis au moyen de la pile. A l'intérieur de la fonction fChemin, ce pointeur est chargé sur la pile pour recevoir à l'adresse qu'il référence une copie de la constante 'c:\dos'. La copie est exécutée à l'aide de la procédure interne appelée StrCopy. En fin de compte, la mémoire tampon contient une copie de la constante 'c:\dos'. Une fois que la fonction fChemin a appelé la procédure StrCopy, elle a rempli son rôle: le pointeur de pile est restauré et la main est rendue au programme appelant. Mais on remarque que le pointeur transmis n'est pas comme d'habitude retiré de la pile. L'explication est que le programme appelant a encore besoin de ce pointeur, comme argument pour un autre appel à StrCopy. Cet appel est nécessaire pour affecter le résultat de la fonction à la variable prévue à cet effet. Ainsi le programme appelant et la fonction appelée travaillent la main dans la main, mais au prix d'une certaine complexité. La fonction fChemin pourrait très bien réaliser directement l'affectation du résultat à la variable NouvoChemin, puisque le pointeur nécessaire lui a été communiqué. Dans ce cas le programme ferait l'économie d'une routine de copie toujours importante consommatrice de temps. La deuxième fonction chaîne montre que dans le cas des chaînes standard Turbo Pascal utilise le même processus fastidieux: l'installation du tampon nécessite une copie supplémentaire du résultat. Les spécialistes de Borland pourront rechercher dans ce domaine quelques idées d'améliorations pour les versions ultérieures du langage. Pour terminer voici une récapitulation des différentes manières dont les fonctions retournent leurs résultats en fonction de leur type: Registres ulilisôs pour retourner les résultats des fonctions

Récursion

Les fonctions récursives ont longtemps été, pour de nombreuses générations de programmeurs, le dernier mystère à percer: je suis moi-même passé par là. Faute de pouvoir être appréhendé de façon rationnelle, ce domaine a souvent conservé une aura mystique, qu'on ne retrouve plus par ailleurs dans la société informatisée, sauf peut-être dans les jeux d'aventure. Il a des choses qui fonctionnent sans qu'on sache exactement comment, mais après tout du moment que ça marche, que souhaiter de plus?

En fait les calculs récursifs ne font pas appel à des ressources particulières du processeur, leur maniement nécessite simplement un peu plus de matière grise. Pour le processeur, il n'y a là rien de spécial. Les appels de fonctions et de procédures récursives sont pour lui du domaine de l'ordinaire, il ne s'occupe pas du fait qu'une procédure s'appelle elle-même et engage une récursion plutôt que d'appeler une autre procédure.

Les chapitres précédents consacrés à la gestion des variables locales et la transmission des paramètres ont révélé indirectement la raison de ce comportement. Il ne s'agit plus pour nous que de rassembler les pièces du puzzle.

Pour cela nous allons nous aider d'une fonction récursive très simple : la fonction factorielle. Cette fonction, à vrai dire, serait plus efficace si nous la programmions avec le schéma itératif classique. Mais comme elle fonctionne aussi sous forme de récursion simple et brève, nous allons l'étudier en détail.

La factorielle est issue de la théorie des probabilités et sert à trouver la réponse à des problèmes du genre:

"Combien d'arrangements peut-on réaliser avec trois cartes de couleur différente (rouge, verte et bleue) disposées l'une à côté de l'autre ?" La solution est facile à construire. Pour la carte de gauche, il existe 3 possibilités, car chacune des 3 cartes convient. Pour la carte du milieu, il n'existe plus que 2 possibilités, car l'une des cartes est déjà utilisée. Pour la troisième carte, il ne reste plus qu'une possibilité correspondant à la dernière carte disponible. Le nombre total de possibilités est donc 3*2*1 = 6. C'est ce type de calcul que réalise la factorielle FAC, définie récursivement de la façon suivante: FAC(n) - n * FAC(n-1) avec FAC(1) - 1 I L'exemple suivant montre comment cette définition peut être programmée. program factorielle-,.j -I var result : longint;J function fac( x : longint ) : longint;.J -I beginJ cs:0000 55 push bp ;Initialiser la pile et réserver )J cs:0001 89E5 mov bp.sp ; 4 octets pour le résultat de )J cs:0003 83EC04 sub sp,0004 ; la fonction Dans les coulisses du langage 2-69 La Bible Turbo Pascal 2-70 If ( x - 1 ) then.J cs:0006 837E0600 cnip word ptr [X+2],0000 ;Mot fort - O ( cs:000A 7512 jne ELSE ;Non - --> ELSE ).j cs:000C 837E0401 cmp word ptr [X].0001 ;Mot faible - 1 ? ).j ( cs:0010 750C me ELSE ;Non - - -> ELSE ).J .J fac :- 1 ( X vaut 1. termine la récursion. retourne 1 ).i f cs:0012 C746FC0100 mov word ptr [bp-04].0001 ;Charge 1 dans Li cs:0015 C746FE0000 mov word ptr [bp-02).0000 la var locale ).i f cs:0018 EB20 jmp END ;Fin de la fonct.LJ -J else f X ne vaut pas 11.J fac x * fac( x-1 ); f Effectue la récursion avec X-1 ).J cs:0010 C44604 les ax,[bp+04] ;Charge X en ES:AX cs:0020 8CC2 mov dx.es et de là en DX:AX cs:0022 200100 sub ax.0001 :Calcule X-1 f cs:0025 83DA00 sbb dx,0000 cs:0026 52 push dx et le transmet sur la ).J cs:0027 50 push ax pile pour appeler la ).J cs:0028 E8D6FF call FAC fonction RECURSIVEI )j ;Le résultat de la fonction ).J se trouve maintenant en DX:AXLJ cs:002B C44EO4 les cx.[bp+04] Charge X en ES:CX cs:002E 8CC3 mov bx.es et de là en BX:CX cs:0030 9A0B02975F call LongMul ;Multiplie X par le résultat de la fonction cs:0035 8946FC mov [bp-04],ax ;Stocke le produit comme f cs:0038 8956FE mov [bp-02].dx résultat de la fonction Li -J end;.J cs:003B C446FC les ax.[bp-04] ;Met le résultat en ES:AX Li f cs:003D 8CC2 mov dx,es ; et de là en DX:AX f cs:003F 89EC mov sp.bp ;Restaure la pile cs:0041 5D pop bp ;Avec 2 POP BP c'est plus f cs:0041 5D pop bp rapide qu'avec ADO SP,2 f cs:0042 C20400 ret 0004 ;Retour à l'appelant avec f nettoyage du paramètre X Li -J begi n.J result fac( 3 );.J f cs:0040 BOOAOO mov ax,0003 ;Charge 3 comme Longlnt ).i f cs:0050 31D2 xor dx.dx en DX:AX )-i f cs:0052 52 push dx le dispose comme Li f cs:0053 50 push ax paramètre sur la pile Li f cs:0054 E8A9FF call FAC :Appelle la factorielle ).i f cs:0057 A33E00 mov [RESULT],ax :Transfère le résultat de Li cs;005A 89164000 mov [RESULT+2].dx DX:AX à la variable RESULT).J end. La fonction factorielle FAC calcule la factorielle de 3 à l'intérieur du programme principal. L'appel de FAC se réalise d'une manière semblable à n'importe quel appel de fonction. L'argument est empilé, la fonction déclenchée, le résultat transmis dans la variable prévue. La fonction FAC elle-même ne se distingue en rien d'une fonction ordinaire. La pile est d'abord initialisée et quatre octets sont réservés pour recevoir le résultat de la fonction. Le paramètre transmis est alors chargé et comparé à la valeur 1. Si tel est le cas, le résultat est mis à 1, ce qui permet de terminer la fonction et une éventuelle récursion par un branchement vers la fin. A cet endroit, le résultat de la fonction est transféré de la variable locale à la paire de registres DX:AX, la pile est restaurée et la main est ensuite rendue au programme appelant. Si le paramètre X contient une valeur différente de 1, il se produit un appel récursif. X est d'abord chargé dans les registres du processeur, décrémenté de 1 et mis sur la 2-70 La Bible Turbo Pascal 2-71 Dans les coulisses du langage pile. Ensuite FAC s'appelle lui-même, ce qui constitue la récursion proprement dite. Quand cet appel est terminé, le programme appelant reprend le contrôle et trouve dans les registres DX:ÂX le résultat de FAC(X-1). Ce résultat est multiplié par X et mis sur la pile comme résultat de la fonction. Cette explication a l'air simple, mais en réalité le processus est plus complexe qu'il n'y paraît au premier abord. L'appel de FAC(X-1) provoque en effet à son tour X récursions. La pile joue ici un rôle essentiel. A chaque appel récursif le registre BP se rapproche du début de la pile chaque appel de FAC entraîne un nouvel empilage des paramètres de la fonction (ici un seul) et des variables locales (ici il n'y en a pas) et un nouveau stockage du résultat. Ce n'est qu'au prix de ce mécanisme que FAC peut retrouver à l'issue de la récursion son paramètre X laissé intact sur la pile. Si l'adressage des paramètres et des variables locales n'était pas relatif au registre BP en déplacement, les différents appels récursifs se mélangeraient et conduiraient au plantage du système. 1. Appel de FAC (3) 2. FAC (3): lriltlaraaton de la pile 3. FAC (3): Appel récursif

Paramétra :3 Paramètre :3 Paramètre :3

Adresse de retour Adresse de retour Adresse de retour au programme - sp au programme au programme principal princaJ pflncjrat

BP de rappelant BP de rappelant BP Résultat do la Résultat do la fonction FAC(3) fonction FAC(3) sP Paramatre:2

Adresses do retour * FAC (3) - sP

- SS:0000 - SS:0000 SS:0000

Dans les coulisses du langage 2-71 La Bible Turbo Pascal 2-72 4. FAC (2) lnaIatn de la plie 5. FAC (2) Appel récursif 6. FAC (1): lnifiahsation de la pile

Paramètre: 3 Paramètre 3 Paramètre: 3

Adresse de retour Adresse de retour Adresse de retour au programme au programme au programme principal principal principal BP de rappelant BP de rappelant BP de Iappelarmt

Résultai de la Résultai do la Résultai de la

fonction FAC (3) fonction FAC (3) fonction FAC (3) Paramètre: 2 Paramètre 2 Paramètre : 2

Adresse de retour Adresse de retour Adresse de retour

à FAC (3) à FAC (3) à FAC (3) BP de FAC (3) BP do FAC (3) BP de FAC (3)

0—BP Résultai de la Résultai de la Résultai do la

fonction FAC (2) fonction FAC (2) fonction FAC (2) •0— SP

Paramètre: 1 Paramètre:1

Adresse de retour Adresse de retour

à FAC (2) à FAC (2) '0— SP BP de FAC (2)

0— BP

Résultat de la fonction FAC(1) .1 '4— SP


'* SS:0000 '4— SS:0000 '0— SS:0000

7. FAC (1) Retour de rècursion 8. FAC (2) Retour rie récursion 9 FAC (3) Retour au programme principal '0— SP Paramètre 3 Paramètre 3

Adresse de retour Adresse de retour au programme au programme principal principal BP de rappelant BP de rappelant

'0- BP Résultat de ta Résultat de la

fonction FAC (3) fonction FAC(3) - 6 '4— SP Paramètre 2


Adresse de retour FAC (3)

BP de FAC (3) '0— BP Résultat de la

fonction FAC (2) - 2 '0— SP

SSOOOO '0SSOOOO 'O—SS 0000

Figure 21: Modification du contenu de la pile pendant 'exécution de FAC(3) 2-72 La Bible Turbo Pascal 2-73 Dans les coulisses du langage

Pointeurs

Un nouveau mode de pensée ........................................73 Les pointeurs dans la programmation système ........................79 W1TH et les pointeurs ..............................................83 Arithmétique des pointeurs .........................................87 Les pointeurs de code ..............................................89 Les pointeurs jouent un rôle important dans la programmation en Pascal. Qu'ils servent à réaliser des listes chaînées ou des arbres binaires, ce sont toujours des pointeurs qui permettent de relier entre eux des éléments appartenant à des structures de données complexes. Les pointeurs sont également importants dans la programmation système, notamment lorsqu'on travaille avec des fonctions du DOS ou du BIOS. Chaque fois que les fonctions appelées manipulent des tampons et des structures de données, ce sont des pointeurs qui sont mis en service pour les référencer. Il y a donc bien des raisons d'étudier la structure, l'utilisation et les possibilités des pointeurs en Turbo Pascal.

Un nouveau mode de pensée

Bien que les pointeurs soient une notion importante en Pascal, ils offrent quelques difficultés aux programmeurs, notamment aux débutants. Pour comprendre leur fonctionnement il faut en général réformer un peu sa manière de penser. Il peut s'écouler un temps assez long jusqu'à ce que la lumière se fasse et ramène les pointeurs au rang d'objets tout à fait ordinaires dela programmation. Il faut bien se rendre compte que les pointeurs n'ont pas un contenu semblable à celui des variables de type Char, Integer, Boolean mais qu'ils contiennent une valeur interprétée par Turbo Pascal comme un numéro de mémoire (vive ou morte). Le type de la variable qui se trouve à l'adresse indiquée doit être précisée au moment de la définition du pointeur. Ainsi: I var IP: Alnteger; I signifie que IP est un pointeur qui pointe sur une variable entière. Turbo Pascal suppose alors que la mémoire adressée par le pointeur contient un nombre entier. Il est par conséquent interdit d'utiliser le pointeur pour stocker l'adresse d'un autre type de donnée. La séquence: var IP: Alnteger;J I: integer.J C: char;.J begin.J IP:— 01. C l'adresse de I est mise en IP ).J IP:— @c;-1 end; Dans les coulisses du langage 2-73 La Bible Turbo Pascal 2-74 provoque une erreur de compilation. En effet le pointeur déclaré comme un pointeur d'entier ne peut pas pointer sur un caractère. L'adresse mémorisée dans le pointeur ne servirait pas à grand-chose si on ne pouvait pas, par son intermédiaire, accéder à une variable, c'est-à-dire la "référencer". La référence est effectuée par une instruction du type: Il a très précisément la signification suivante: "Chercher le contenu du pointeur IP, le considérer comme l'adresse d'une variable entière en mémoire centrale et stocker dans cette variable la valeur 6". Les implications sur le code généré par Turbo Pascal sont illustrées par l'exemple suivant. program EasyPtr;.J J var I : lnteger;.J ip : integer:.J C : char;.J cp char;.J

begi n.J ip :- @i; ( IP pointe sur I cs:0008 883E00 mov ax.offset I ;Charge en AX le déplacement ).J { ; de I cs:000B 8COÀ mov dx.ds ; I se trouve dans le segment ).i de données cs:0000 A34000 mov [IP],ax ;Stocke en IP l'adresse de ).J cs:0010 89164200 mov [IP-'-2],dx : segment et le déplacement ).J ( ; de I cp @c; ( CP pointe sur C ).J cs:0014 884400 mov ax.offset C ;Charge en AX le déplacement ).J C ;deC.J cs:0017 8CDA mov dx,ds ;C se trouve lui aussi dans ).J le segment de données cs:0019 A34500 mov [CP],ax ;Stocke en CP l'adresse et ).J cs:001C 89164700 mov [CP+2],dx le déplacement de C J 5; 1 I vaut 5 cs:0020 C7063E000500 mov word ptr [I].0005 ;affectation directe ).J J lp 6; 1 IP pointe sur 6 ).J cs:0026 C43E4000 les dl,[IP] ;Charge le contenu de IP ).j ( ; en ES:DIJ C cs:002A 26C7050600 mov es:word ptr [di],0006 ;Charge 6 en [ES:DI]LJ J C :— 'X ;J cs:002F C606440078 mov byte ptr [C]. x' ;Affectation directe ).J J cp :- u'; j transforme X en U j cs:0034 C43E4500 les di,[CP] ;Charge le contenu de CP ),J ( ; en ES:0I j cs:0038 26C60575 mov es:byte ptr [di]. 'u' ;Charge 'u' en ES:DI]}.J end. Ces instructions montrent la manière dont sont créés les pointeurs, affectées les adresses et référencées les variables. Le programme définit des variables de types 2-74 La Bible Turbo Pascal 2-75 Dans les coulisses du langage Char et Integer, ainsi que deux pointeurs appelés IP et CP. IP pointe sur des variables de type Integer, IC sur des variables de type Char. Pour permettre d'accéder véritablement à des variables, les pointeurs doivent d'abord recevoir des adresses de variables: sinon leur contenu est indéterminé et ils pointent sur une mémoire quelconque tout à fait aléatoire. Il est donc recommandé d'initialiser les pointeurs avant d'écrire à l'endroit qu'ils référencent. A cet effet on utilise souvent l'opérateur "adresse de" représenté par @ et qui retourne l'adresse d'une variable. Ce résultat peut alors être affecté à un pointeur, à condition que le type du pointeur soit compatible avec le type de la variable. Dans notre exemple, les deux pointeurs reçoivent respectivement l'adresse de la variable entière et celle de la variable caractère. A l'issue de cette affectation, IP pointe sur Jet Œ sur C. Comme il a été expliqué au chapitre 2.3, Turbo Pascal gère les pointeurs indépendamment du type des variables pointées: ils occupent toujours deux mots mémoire dont l'un contient l'adresse du segment et l'autre le déplacement de la variable référencée. Ce mécanisme apparaît clairement au moment où les deux pointeurs chargent les adresses des variables. Turbo Pascal met d'abord les adresses des variables dans la paire de registres DX:AX et les transfère ensuite dans les pointeurs. Après la phase d'initialisation des pointeurs, on affecte la valeur 5 à I. Aucun pointeur n'entre en jeu de sorte que Turbo Pascal peut écrire directement le nombre 5 dans la mémoire prévue pour I. Mais les choses se passent différemment lorsque le pointeur IP référence la variable. Dans un premier temps Turbo Pascal est obligé de charger le contenu du pointeur, c'est-à-dire l'adresse de la variable référencée, dans les registres du processeur. Comme cette opération est fréquente dans la programmation sur PC, le processeur 8086/8088 a été pourvu d'une instruction spéciale appelée LES. LES s'emploie toujours avec le nom d'un registre et une adresse mémoire qui contient le pointeur à charger. La partie segment du pointeur est chargée en ES, la partie déplacement dans le registre indiqué. Turbo Pascal utilise pratiquement toujours l'instruction LES en liaison avec le registre DI: ainsi le contenu du pointeur se retrouve dans la paire de registres ES:DI. Turbo Pascal a donc en mains l'adresse de la variable reférencée et peut facilement accéder à la variable par le jeu de l'adressage indexé de base ES et d'index DI. Notez bien les mots-clés "word ptr" que Turbo Pascal ajoute à l'instruction MOV lorsqu'elle écrit la valeur 6 dans la variable. Ils signifient que l'instruction MOV doit accéder à un mot car, comme nous l'avons vu au chapitre 2.3, les variables de type Integer sont gérées au format interne WORD. La variable pointée par CP étant de type Char, on constate une différence dans le code généré pour manier le deuxième pointeur. Le contenu de CP est d'abord chargé dans les registres ES:DI, ce qui est identique au cas précédent. Mais ensuite l'instruction MOV est utilisée avec "byte ptr" au lieu de "word ptr": car les variables caractères sont représentés dans le format interne BYTE. D'un type à l'autre, le format interne du pointeur ne change pas: ce qui change, ce sont les instructions mises en place pour accéder aux variables pointées. Dans les coulisses du langage 2-75 La Bible Turbo Pascal 2-76 Ainsi si on référence un entier long, Turbo Pascal ne peut pas se satisfaire d'un ordre MOV unique. Ce type de variable occupe deux mots en mémoire centrale. Ii faudra donc que le compilateur génère deux instructions MOV. Les accès aux réels (qui occupent 6 octets) sont encore plus compliqués. Pas moins de trois MOV sont nécessaires pour charger les trois mots qui constituent une variable de type Real. Lorsque nous disions tout à l'heure que Turbo Pascal interdisait d'affecter une adresse de caractère à un pointeur d'entier, il ne s'agissait en fait que d'une demi-vérité. Turbo Pascal étend aux pointeurs les règles de compatibilité instituées pour les variables ordinaires, ce qui ouvre la voie à quelques échappées dangereuses. A l'intérieur du groupe des variables entières, c'est-à-dire des types Byte, Shortlnt, Integer, Word et Longlnt, il est permis de mélanger les affectations. lien est de même pour les pointeurs correspondants. Comme le montre le listing ci-dessous, Turbo Pascal n'interdit pas au programmeur d'affecter l'adresse d'une variable de type Integer ou Byte à un pointeur d'entier long Longlnt. Cette affectation n'a pas de conséquence négative directe mais si le pointeur effectue une référence, Turbo Pascal considère que son contenu représente l'adresse d'un entier long et écrit donc 4 octets en mémoire, alors que seuls un ou deux octets ont été réservés pour la variable. Un certain nombre de mémoires qui contenaient d'autres variables sont donc écrasées, ce qui conduit généralement à une erreur d'exécution du programme. program wrongptr;.J .J I et octet situé après I sont écrasés )J var ip: Alonglnt;J b: byte-.-J f Le piège... I: integer;.J Les deux octets situés après I sont écrasés ).J beglnj ip:— b; ( Danger .... 1p": $12345678; ip:— @i lp:— $12345678; end. Les pointeurs décrits jusqu'ici faisaient partie de la catégorie des "pointeurs typés" qui sont liés à un certain type de donnée. Ce lien permet à Turbo Pascal de connaître la taille et le format des variables pointées et de générer les instructions appropriées lorsque les pointeurs s'y réfèrent. Les pointeurs génériques sont au contraire des pointeurs non typés qui portent simplement la dénomination "pointer". Turbo Pascal ne leur associe aucun type de donnée et interdit toute opération de référencement: en effet un pointeur générique ne connaît ni le format ni la taille de la variable à référencer. Malgré tout, il est nécessaire de disposer de ce genre depointeur "asexué". Il existe en effet toute une série de fonctions et de procédures (par ex GetMem et Ptr) qui retournent des pointeurs qui ne doivent pas avoir de type précis, sans quoi elles n'auraient pas de portée générale. Un pointeur générique présente l'avantage de pouvoir être affecté à tout autre pointeur, qu'il soit typé ou non. 2-76 La Bible Turbo Pascal 2-77 Dans les coulisses du langage Pour qu'un pointeur générique puisse référencer une variable, il reste la possibilité de le soumettre à un transtypage qui le convertira en un pointeur de n'importe quel type souhaité. Ce processus est simple et ne pose pas de problème, comme le montre le listing suivant qui affiche la date de la dernière mise à jour du BIOS de votre ordinateur. Cette information se situe à l'intérieur des derniers 16 octets de l'espace de I Mo de la mémoire centrale, sous la forme de 8 codes ASCII. La position exacte de la date à l'intérieur des 16 octets varie quelque peu en fonction de la version du BIOS: c'est pour cette raison que le programme commence par rechercher le premier chiffre ASCII du domaine. Pour accéder aux mémoires mortes concernées, le programme se sert de la fonction PTR et de diverses déclarations de types. Il déclare ainsi un tableau de 16 caractères appelé BiosDate, auquel est associé un type de pointeur BDPTR. Pour signaler à Turbo Pascal qu'un tableau de type BiosDate se trouve à l'adresse F000:FFFO, le programme appelle la fonction PTR avec pour arguments le segment et le déplacement de l'adresse. Le résultat de la fonction est un pointeur générique qui pointe sur la mémoire souhaitée. On convertit ensuite par transtypage ce pointeur générique en un pointeur de tableau BDPTR. Le référencement de ce dernier pointeur fournit l'adresse de base du tableau en mémoire. En prenant comme index un compteur I, on peut alors par le moyen d'une boucle accéder successivement à tous les éléments de ce tableau. A partir de ce moment, Turbo Pascal ne fait plus de différence entre ce tableau situé à la fin de l'espace mémoire et les tableaux ordinaires situés dans le segment de données ou la pile. A titre de récapitulation, voici encore une fois l'explication des principales étapes: plr($F000,$FFFO) fournit un pointeur générique qui pointe sur la mémoire F000:FFFQ BDPTR(ptr($F000,$FFFO)) S fournit un pointeur qui pointe sur un tableau de type BiosDate situé à l'emplacement mémoire F000:17FFØ. BDPTR(ptr($F000,$FFFO))A[i] fournit le i-ème caractère du tableau de type BiosDate qui se trouve à l'emplacement mémoire 17000:1717170. L'ensemble va maintenant être réuni dans un programme qui va vous révéler que votre PC au design futuristique abrite un BIOS remontant aux temps anciens. Avant de décrocher votre téléphone pour protester auprès du vendeur, assurez-vous d'avoir bien interprété les informations apparues à l'écran. La date se trouve en effet écrite au format américain (MM/JJ/AA). program PtrBios;.J J- type BiosDate - array[1. .16] 0f char;J BDPTR - ABiosDate;J J var 1. J : lnteger;J J beglnJ write( Date du BIOS: ');J Dans les coulisses du langage 2-77 La Bible Turbo Pascal 2-78 f-- Recherche le premier chiffre ---------------------------- ------------

J

I i;.-J whlle f BDPTR(ptr( SF000. SFFFO ))*[i]<O' ) orJ BDPTR(ptr( SF000. $FFFO ))[i]>'9' ) do.J inc( I ); t Pas de chiffre, on continue ).j J f-- Chiffre trouvé, on affiche 8 caractères ------------------------------ .J for j O to 7 doJ write( BDPTR(ptr( SF000, SFFFO ))[1+j] );J J writeln;..J end. Vous allez retrouver le transtypage des pointeurs génériques au prochain chapitre, lorsqu'il s'agira de programmation système. Mais auparavant vous devrez encore prendre quelques précautions pour être capable non seulement de changer de mode de pensée, mais encore de révolutionner complètement le fonctionnement de votre esprit. Pour commencer, je voudrais vous rendre attentif à une source d'erreur liée aux pointeurs et qui a fait passer des heures fort désagréables à nombre de programmeurs Pascal. L'erreur concerne l'utilisation de pointeurs avec les procédures FillChar et Move proposées par l'unité System. La procédure FillChar remplit une zone mémoire avec un caractère répétitif constant et attend comme premier paramètre une référence à la variable à remplir. La procédure Move copie une zone mémoire (source) dans une autre (destination) et attend comme paramètres des références aux deux zones source et destination ainsi que le nombre d'octets à copier. L'une et l'autre procédure sont souvent utilisées avec des pointeurs. Elles posent problème si on transmet des pointeurs au lieu de transmettre les adresses des variables pointées. Conséquence de cette erreur: avec FillChar le pointeur est lui-même rempli avec le caractère répétitif qui s'inscrit également dans les mémoires suivantes. Avec Move, ce sont les zones qui commencent aux deux pointeurs qui sont en fait concernées par la copie. Le problème survient parce que les deux procédures sont prévues pour recevoir des paramètres non typés. II n'y a donc aucune vérification des types à la compilation. Si on indique un pointeur comme paramètre, il est pris pour une variable dont l'adresse est transmise à la procédure selon le schéma classique. Il est possible d'éviter ce malentendu en référençant les pointeurs, comme le montre l'exemple suivant: program MoveFillChar;J var pi. t Deux pointeurs génériques )J p2 : pointer;.-' beglnJ GetMem( pi, 100 ); C Réserve 100 octets, affecte l'adresse à Pi )J GetMem( p2. 400 ); C Réserve 400 octets, affecte l'adresse à P2 ).i J FillChar( pi. iOO. 'X' ); C Erreur conduisant généralement à un crash I )J FillChar(piA. 100, 'X' ); C Instruction correcte )J J Move( p2, pi. iDO ); t Ne marche pas non plus I )J Move( p2A, p1, 100 ); C Instruction correcte )J end.

Les pointeurs dans la programmation système

Les pointeurs jouent un grand rôle dans la programmation système, car beaucoup de fonctions du DOS et du BIOS utilisent des pointeurs dès qu'interviennent des tampons et des structures de données. Que ces fonctions attendent des pointeurs comme arguments d'entrée ou retournent des pointeurs comme résultats, il faut connaître leur fonctionnement, leur structure et les possibilités de transtypage pour maîtriser le dialogue entre le programme et le système de base. La présente section se propose de vous présenter un exemple pratique de programmation système pour vous familiariser avec les problèmes qu'elle pose. Nous nous intéresserons plus spécialement à un algorithme capable de nous indiquer les désignations alphabétiques des lecteurs installés sous DOS. Bien que de nombreuses applications aient besoin de ce genre de routine, par exemple pour permettre à l'utilisateur de sélectionner un lecteur, le système DOS ne proposait jusqu'à la version 3.0 aucune fonction capable de donner cette information. Il fallait alors se rabattre sur des structures de DOS non documentées et par conséquent secrètes. Note: A partir de la version 3.0 on peut se servir d'une fonction DOS initialement prévue pour autre chose mais que l'on peut détourner pour connaître le numéro du lecteur. L'algorithme présenté ici fonctionne parfaitement mais il ne tient pas compte des disques virtuels qui à partir de la version 3.0 peuvent être installés par les commandes SUBST et JOIN. Les désignations des lecteurs font partie d'une structure de données appelée 'bloc des paramètres du lecteur' (Drive Parameter Bloc = DPB) que DOS crée à titre interne pour chaque lecteur géré. Comme le montre le tableau suivant, un DPB contient toute une série de précieuses informations sur le lecteur décrit. Pour notre part, nous ne nous servirons que de deux champs: le premier qui donne le numéro du lecteur, et le dernier qui est un pointeur de chaînage renvoyant sur un autre DPB. Adiesse Contenu Type +OOh Numéro du lecteur (O = A, 1 = B etc.) 1 BYTE +Olh Sous-unité du driver pilotant le lecteur 1 BYTE +02h Nombre d'octets par secteur 1 WORD +04h Facteur_d'Interleave 1 BYTE +05h Nombre de secteurs par cluster 1 BYTE +06h Secteurs réservés (Boot) 1 WORD +08h Nombre de tables d'allocation (FAII 1 BYTE +09h Nombre d'entrées du répertoire principal 1 WORD +OBh Premier secteur occupé 1 WORD +ODh Dernier secteur occupé 1 WORD +OFh Nombre de secteurs par FAT 1 BYTE Dans les coulisses du langage 2-79 La Bible Turbo Pascal 2-80 +10h Premier secteur de données 1 WORD + 12h Pointeur sur l'en-tête du driver associé 1 PTR +16h Descripteur de support 1 BYTE + 17h Indicateur de mise en service 1 BYTE (OFFh = Lecteur non encore utilisé) + 18h Pointeur sur le DPB suivant 1 PTR (xxxx:FFFF =plus DPB ) _de_ Longueur: lCh (28 octets) Figure 22 Structure du bloc de paramètres d'un lecteur (DPB) Pour retirer des informations des DPB, il faut d'abord être capable d'accéder à la liste chaînée, c'est-à-dire connaître l'adresse du premier DPB. Mais là encore, DOS ne donne pas spontanément cette information. Il s'avère nécessaire de recourir à une autre structure de données interne: le bloc d'informations de DOS (Dos Information Bloc = DIB). Le premier champ de cette structure est justement un pointeur qui pointe sur le premier DPB. +OOh Pointeur sur le premier bloc de paramètres d'un 1 PTR lecteur (DPB) +04h Pointeur sur le dernier tampon DOS 1 PTR +08h Pointeur sur le pilote de l'horloge ($CLOCK) 1 PTR +OCh Pointeur sur le pilote de la console (CON) 1 PTR +10h Taille maximale d'un secteur 1 WORD +12h Pointeur sur le premier tampon DOS 1 PTR + 16h Pointeur sur la table des chemins d'accès 1 PTR + lAh Pointeur sur la table des fichiers système (System_ File 1 WORD _Table _SFT) Longueur: lEh (30 octets) Figure 23 Structure du bloc d'informations de DOS (DIB) Connaissant la structure des DPB et du DIB, nous sommes presque arrivés au bout de nos peines. Il ne nous manque plus que l'adresse du DIB. Cette dernière peut être obtenue par une fonction secrète de DOS qui porte le numéro $52 et ne sert qu'à cette fin. Si on dispose le numéro $52 dans le registre AH, la fonction retourne l'adresse du DIB dans la paire de registres ES:BX. 2-80 La Bible Turbo Pascal 2-81 Dans les coulisses du langage Les différentes étapes pour trouver les désignations des lecteurs en service seront donc les suivantes: • appeler la fonction $52 • considérer le contenu de ES:BX comme un pointeur sur le DIB lire dans le DIB un pointeur sur le premier DPB • passer en revue les DPB jusqu'à ce que dans le champ de chaînage se trouve l'offset $FFFF; en même temps afficher ou stocker en mémoire les numéros des lecteurs. La transposition de ces opérations en instructions Pascal n'est pas aussi triviale qu'il semble au premier abord. Il faut d'abord reproduire les structures DOS avec des enregistrements Pascal: c'est le seul moyen d'en manipuler rapidement les informations. Les adresses des champs des enregistrements Pascal doivent donc correspondre exactement aux adresses des champs de la structure DOS, sans quoi il sera impossible d'interpréter correctement les données. La construction de ces enregistrements est chose facile: on donne à chaque champ la taille de son homologue de la structure DOS, et on veille à n'en oublier aucun. Dans son fonctionnement interne, DOS utilise des types de données qui correspondent à des types Pascal: les champs de 1 Byte correspondent au type Pascal Byte • les champs de 2 Bytes correspondent aux types Pascal Integer ou Word selon l'interprétation qu'on donne au bit le plus significatif • les champs de 4 Bytes peuvent être considérés comme des entiers longs (Longlnt) lorsqu'ils contiennent des Informations numériques ou comme des pointeurs. En imitant une structure de données de ce genre, vous n'êtes pas obligé de donner un nom et un type à chaque champ. Vous pouvez vous limiter aux champs qui serviront réellement dans le programme. Il y aura donc entre les différents champs des lacunes qu'il faudra combler pour respecter la correspondance étroite entre l'enregistrement Pascal et la structure DOS. On utilise à cet effet des variables fantômes sous forme de tableaux d'octets. Chacun de ces tableaux doit contenir autant d'octets que nécessaire pour assurer la jointure entre les champs. Si DOS fournit un pointeur sur une de ses structures, on peut utiliser le transtypage pour faire croire à Turbo Pascal que ce pointeur pointe sur la structure Pascal associée. Par le jeu des référencements on peut alors accéder sans problème à n'importe quel champ de la structure. Le programme suivant qui affiche les désignations des lecteurs installés applique les principes énoncés au bloc des paramètres de DOS. Dans les coulisses du langage 2-81 La Bible Turbo Pascal 2-82 program getdrvs;.J -J uses Dos;J .J procedure GetDrlves;.J J type DPBPTR - DPB; f Pointe sur un bloc de paramètres de DOS )J OPBPTRPTR - DPBPTR; f Pointe sur un pointeur de DPB ).i DPB - record f Emule un bloc de paramètres de DOS ).j Nuni byte; f Numéro du lecteur (O—A, 1—B etc. ).J dummy : array [l..$17] 0f byte; ( Sns intérêt pour nous ).j Next : DPBPTR; f Pointe sur le prochain DPB ).J end; f xxxx:FFFF indique le dernier DPB ).J -J var Regs : Registers; Registres servant à l'interruption ) .J XDpbP : DPBPTR; Pointeur courant sur un DPB ).J .J begin.J f-- Recherche le pointeur sur le premier DPB 'J Regs.AH :- $52; ( La fonction $52 retourne un pointeur sur le 018 ).J MsDos( Regs );_J XDpbP :- DPBPTRPTR( ptr( Regs.ES, Regs.BX ) )';.J f cs:OO1C 8B46EE mov ax.[bp-12] ;Charge Regs.BX en AX f cs:OO1F 8B56FC mov dx.[bp-04] ;Charge Regs.ES en OS f cs:0022 89C7 mov dl,ax ;Transfère le déplacement de ).i f ; AX en DI.J cs:0024 8EC2 mov es,dx ;Transfère le segment de f ;DSenES f cs:0026 26C405 les ax.es:[di] ;Transfère le pointeur de )J f ; ES:DI en ES:AX L- f cs:0029 8CC2 mov dx.es ; puis en DX:AX f cs:0028 8946E8 mov [bp-18],ax ;Ecrit ce pointeur dans la Li cs:002E 8956EA mov [bp-16].dx ; variable XDpbP -J f-- Passe en revue les DPB ------------------------------------------------- .J repeat writeln( chr( ord('A') + XDpbP.Num ) ); f Affiche la désignation ) J ( du lecteur ).i XDpbP :- XDpbP.Next; f Pointe sur le DPB suivant L- until f Ofs( XDpbP ) - $FFFF ); f jusqu'au dernier . . . . end; -J begin.J wrlteln(#13#10'Lecteurs installés : '#13#10);.J GetDrlves ;.J end. Le bloc des paramètres de DOS est ici émulé par une structure dénommée DPB. Dans cette structure, seuls les deux champs qui nous intéressent sont appelés par leur nom, les autres sont rangés en commun dans une variable fantôme. On définit ensuite le type DPBPTR pour pointer sur le bloc. L'enregistrement correspondant au DPI (Bloc d'informations de DOS) n'apparaît pas dans le programme. Il n'est pas nécessaire de le définir, car l'information recherchée (le pointeur sur le premier DPB) se trouve dans le premier champ de la structure. Le pointeur retourné par la fonction DOS numéro $52 pointe sur un pointeur qui donne l'adresse du premier DPB en mémoire. Un pointeur qui pointe sur un autre pointeur: voilà qui paraît plutôt compliqué. Mais Turbo Pascal ne voit pas d'inconvénient à ce genre de construction, comme le montre la déclaration du type DPBPTRPTR. Ce type entre en service à la suite de l'appel de - 2-82 La Bible Turbo Pascal 2-83 Dans les coulisses du langage la fonction $52 quand il s'agit de mettre dans le pointeur XDpbP l'adresse du premier DPB. L'opération est effectuée par l'instruction: XDpbP :- DPBPTRPTR( ptr( Regs.ES, Regs.BX ) )A; I La fonction Ptr livre un pointeur générique composé des contenus des registres ES et BX. Ce pointeur est alors transtypé en DPBPTRPTR, c'est-à-dire en un pointeur de pointeur de DPB. Cette expression est alors référencée et le résultat est affecté à la variable XDpbP. Dans cette référence Turbo Pascal transfère d'abord un pointeur de l'adresse indiquée par le résultat de Ptr vers les registres ES:DI. Ces registres pointent donc sur le début du DIB en mémoire. Le pointeur mémorisé à cet endroit est alors transféré dans DX:AX et, de là, dans la variable XDpbP. A ce moment le programme aborde une boucle REPEAT UNTIL dans laquelle il exploite à chaque itération le bloc DPB pointé par XDpbP. La désignation du lecteur est affichée, puis XDpbP est chargé avec l'adresse du bloc suivant qui est inscrite à la fin de chaque DPB. La boucle se termine lorsque le déplacement du pointeur XDpbP a la valeur $FFFF qui indique la fin de la liste des DPB. Ce programme ne constitue qu'un petit exemple d'accès aux structures et aux pointeurs gérés par le système d'exploitation. Mais les techniques présentées ici seront appliquées tout au long de ce livre, que les structures des données à traiter soient plus complexes ou au contraires plus simples. V WITH et les pointeurs Quel rapport y a-t-il entre les instructions WITH et les pointeurs ? Aucun à première vue, car l'instruction WITH est en principe destinée à faciliter l'accès aux composants élémentaires des tableaux d'enregistrements. Il est certes permis d'utiliser un pointeur dans un ordre WITH s'il référence un enregistrement, mais ce n'est pas là le rapport que je veux évoquer. Il s'agit plutôt de la traduction des ordres WITH en code machine: les pointeurs y jouent un rôle important. Chaque fois que dans un ordre WITH Turbo Pascal doit accéder à un enregistrement dont l'adresse n'est pas connue au moment de la compilation, il forme un pointeur qui pointe sur l'enregistrement. Ce pointeur est déposé dans une variable locale automatiquement générée à l'intérieur de la procédure ou fonction concernée. Pour accéder aux champs élémentaires de l'enregistrement, Turbo Pascal charge le pointeur en ES:DI et gère l'adressage indexé par DI. Le listing suivant montre ce mécanisme. program WlthDemoi;.J type Info— record C Un enregistrement Info quelconque ).J Champi : integer;J Champ2 : char;-) ( "J «J Ici on peut mettre d'autres champs . end;.J Dans les coulisses du langage 2-83 La Bible Turbo Pascal 2-84 var Unelnfo : Info; Un enregistrement Info comme variable globale ).i Tablolnfo array[1. .10] cf Info; ( 10 Info dans un tableau ).i integer; C Compteur d'itérations ).J 'J begin.J with Unelnfo do-1 ne génère pas de code machine car l'adresse de Unelnfo est calculable ).J au moment de la compilation -J begin.J Champi :- 1;J C cs:0008 C7063E000100 mov word ptr [Unelnfo.Champi].1 'J Champ2 : 'e' ;.J cs:0011 C606400061 mov byte ptr [Unelnfo,Champ2], 'a' Li end ;.J 'J (-- Initialise Tablolnfo à l'aide de WITH --)j -J for i :- 1 to 10 do' with Tablolnfo[ 1 3 do.i cs:0022 A15FOO mov ax,[I] ;Charge l'index I en AX ).J cs:0025 BA0300 mov dx,0003 ;Charge la long, d'un enreg)J cs:0028 F7E2 mul dx ;Index x long, d'un enreg. ).J cs:002A 8BF8 mov di,ax ;Range le produit en DI ).J cs:002C 81C73E00 add dI,offset Tablolnfo - 3 ;+ Adr, Base-3 ),J I cs:0030 897EFC mov [bp-04],di ;Met le pointeur WITH sur ).J cs:0033 8C5EFE mov [bp-02].ds ;Tablolnfo[i] dans une variable locale 'J begi n.J Champi $4711;.J I cs:0036 C47EFC les C cs:0039 26C7051147 mov -J Champ2 'x';' ( cs:003E C47EFC les I cs:0041 26C6450278 mov end; -J dl,[bp-04] ;Pointeur WITH en ES:DI ).J es:word ptr [dl],4711 ).J dl,[bp-04] ;Pointeur WITH en ES:DI ).i es:byte ptr [dl+02], 'x' ),J J (- Initialise Tablolnfo sans WITH -J for I 1 to 10 do_J begi n.J Tablolnfo[i].Champl :- $4711;.J cs:0059 A15FOO mov ax,[I] ;Charge l'index I en AX ),j ( cs:005C BA0300 mov dx.0003 ;Long. de l'enreg. en DX ).i ( cs:005F F7E2 mul dx ;Multiplie la long, par I ),J cs:0061 8BF8 mov di,ax ;et met le résultat en DI Li cs:0063 C7853E001147 mov word ptr [di-+-Tablolnfo- 3] .4711 -J Tablolnfo[i].Champ2 :- 'x' ;.J C cs:0069 A15FOO mov C cs:006C BA0300 mov ( cs:006F F7E2 mul ( cs:0071 8BF8 mov ( cs:0073 C685400078 mov end ;,J end. ax,[I] ;Il faut encore une fois ).J dx,0003 ;calculer l'adresse de dx ;Tablolnfo[i] tout comme di,ax ;ci-dessus byte ptr [di+Tablolnfc- 3],'x' ),J Au centre de ce programme se trouvent les variables globales Unelnfo et Tablolnfo, qui dépendent de l'enregistrement Info. La variable Unelnfo représente un exemplaire unique de l'enregistrement, tandis que Tablolnfo est constitué par un tableau de dix enregistrements de ce type. L'instruction WITH n'est pas compilée de la même façon dans les deux cas. Dans un premier temps, on accède d'abord, par le moyen d'un WITH, à la variable Unelnfo.

L'adresse de cette variable est complètement déterminée au moment de la compilation. De ce fait, Turbo Pascal n'a pas besoin de pointeur pour accéder aux champs de l'enregistrement : il peut les adresser directement à l'intérieur du segment DS. On voit que l'instruction WITH n'est même pas véritablement traduite en code machine, elle sert uniquement à simplifier l'écriture des ordres Pascal.

Les choses se passent différemment avec le tableau de données. Ses différents éléments sont parcourus par une boucle FOR NEXT et leurs champs sont adressés au moyen d'une instruction WITH. Mais ici Turbo Pascal ne connaît pas encore l'adresse des enregistrements au moment de la compilation, car l'indice I ne prend une valeur concrète qu'au moment de l'exécution de la boucle FOR NEXT. Dans cette situation Turbo Pascal fait appel à une variable locale dans laquelle il reporte l'adresse de l'enregistrement au passage de l'instruction WITH. Comme les éléments d'un tableau sont disposés l'un derrière l'autre d'une façon strictement contiguë, leur adresse s'obtient en multipliant leur indice par la longueur d'un élément du type Info (3 octets).

Ce faisant, Turbo Pascal ne recueille que l'adresse du déplacement de l'enregistrement par rapport au début du tableau en mémoire. Il faut encore ajouter l'adresse de base du tableau. Notez bien que dans cette opération Turbo Pascal n'additionne pas exactement l'adresse de base, mais l'adresse de base diminuée de trois. Cette anomalie apparente est due au mode d'indexation et de mémorisation des tableaux. Lors de la déclaration de Tablolnfo, les indices affectés aux éléments ont été définis de I à 10. Le premier élément d'indice 1 se trouve à l'offset O par rapport au début du tableau, et non à l'offset 3, car on n'a pas défini d'élément d'indice 0. Turbo Pascal devrait donc décrémenter l'indice de I avant de le multiplier par la longueur de l'élément. Mais ceci nécessiterait une instruction machine supplémentaire. Il est facile de l'éviter en retranchant les octets excédentaires à la fin du calcul, au moment de l'addition de l'adresse de base. Le principe arithmétique appliqué est le suivant: >- Base +((Indlce-1)3)= Base + ((Indlce*3) - (1*3)) = >- Base + ((Indlce*3) - 3) = > Base - 3 + (Indice*3) L'adressage des éléments du tableau par l'intermédiaire de l'instruction WITH ne simplifie pas seulement la frappe du programme Pascal, mais elle conduit également à un code machine plus efficace. C'est ce que montre la deuxième boucle FOR NEXT dans laquelle les dix éléments du tableau sont à nouveau initialisés et parcourus. Cette fois-ci, il n'y a plus de WITH. Chaque fois qu'on traite un champ d'un élément du tableau, on est obligé d'indiquer le nom du tableau et l'indice concerné. Turbo Pascal doit recalculer l'adresse de l'élément du tableau pour chaque accès à l'un de ses champs, générant ainsi du code machine supplémentaire qui ralentit inutilement le processus. L'utilisation de l'instruction WITH n'est donc pas seulement une question de confort: c'est un impératif d'efficacité! Dans les coulisses du langage 2-85 La Bible Turbo Pascal 2-86 Cependant, l'instruction WITH peut aussi avoir des effets négatifs sur la vitesse d'exécution. Si l'adresse de l'enregistrement à référencer se trouve déjà mémorisée dans un pointeur, elle doit être transférée dans le pointeur local créé par WITH. Ce transfert ne prend certes que trois instructions machine (plus une instruction pour créer la variable locale WITH au cas où la procédure/ fonction ne déclarerait pas elle-même des variables locales), mais il s'ensuit quand même un ralentissement de l'exécution. L'exemple suivant documente trois cas où Turbo Pascal aurait pu s'économiser la manipulation de la variable locale liée au WITH. Si votre application doit être finement optimisée, vous devrez éviter dans ce cas l'instruction WITH et taper les ordres Pascal complets pour accéder aux différents champs d'un enregistrement. program WithDemo2;J J type Info - record ( Un enregistrement Info quelconque ) J Champi integer;.J Champ2 : char;' 'J ici on peut mettre d'autres champs . end;.J '-J InfoPtr - Info; ( Pointe sur un enregistrement Info ).j J ver Unelnfo : Info; ( Un enregistrement Info comme variable globale Li J procedure test( OP : InfoPtr );.J J begln.J with OP^ do.J C cs:000E C47E04 les di,[bp+04] ;Copier le pointeur transmis )..i en ES:DI Li C cs:0011 897EFC mov [bp-04].di ; et de là dans la variable Li C cs:0014 8C46FE mov [bp-02],es ; locale WITH J begi n.J di.[bp-04] ;Pointeur Issu de la Champi $4711;.J variable locale WITH ).j t cs:0017 C47EFC les es:word ptr [dI],4711 { cs:OO1A 26C7051147 mov di,[bp-04] ; idem 'J es:byte ptr [dl+02].78 Li Champ2 :- 'x ;_J ( cs:OO1F C47EFC les t cs:0022 26C6450278 mov end; end ;.J J procedure testi( var Data : Info );J J begi n.J with Data do .J cs:003B C47E04 les dl.[bp-+-04] ;Copie le pointeur transmis )J ( ; en ES:DI Li cs:003E 897EFC mov [bp-04],di ;et de là dans la variable )J C cs:0041 8C46FE mov [bp-02].es ; locale WITH Li J begi nJ Champi :- S4711;J t cs:0044 C47EFC les dl,[bp-04] ;Pointeur issu de la variable locale WITH )J C cs:0047 26C7051147 mov es:word ptr [di].4711 J Champ2 :- cs:004C C47EFC les dl.[bp-04] ;Pointeur issu de la 2-86 La Bible TurboPascal 2-87 Dans les coulisses du langage variable locale WITH ).J f cs:004F 26C6450278 nov es:byte ptr [dl+02].78 end;.J end :.J begin.i test( (JneInfo );.J testl( Unelnfo );.! end. Cet exemple fait lui aussi intervenir l'enregistrement Info et la variable globale Unelnfo. Mais cette fois la variable est transmise comme paramètre aux deux procédures test et testi : dans le premier cas au moyen de l'opérateur "adresse de" comme pointeur et dans le deuxième cas comme paramètre variable. Malgré le mode de transmission différent au niveau des instructions Pascal, le résultat est le même au niveau du processeur. Dans l'un et l'autre cas Turbo Pascal transmet à la procédure invoquée un pointeur qui pointe sur la variable Uneinfo. En liaison avec l'instruction WITH, les deux procédures accèdent à la variable transmise au moyen de ce pointeur, et en même temps elles créent une variable locale WITH parfaitement inutile. A vrai dire cette dernière variable ne serait utile que si à l'intérieur du bloc WITH le pointeur qui pointe sur la variable était soumis à une manipulation. C'est théoriquement possible, mais nécessairement dénué de sens logique: ainsi toute légitimation de ce mécanisme apparaît fort contestable. Comme il est possible de mettre à l'intérieur d'une procédure ou d'une fonction de nombreuses instructions WITH successives, on peut penser que Turbo Pascal réservera sur la pile la place nécessaire pour installer des variables WITH multiples. Mais tel n'est pas le cas, car dans la mesure du possible, Turbo Pascal se contente de prévoir une seule variable qui puisse fonctionner avec tous les WITH. La multiplication des variables WITH n'est nécessaire que dans le cas où plusieurs WITH sont imbriqués: on est alors obligé de prendre des précautions pour éviter d'écraser prématurément des variables encore significatives. I, Arithmétique des pointeurs A l'inverse du langage C, Pascal ne connaît pas l'arithmétique des pointeurs: on ne peut donc pas directement manipuler ces derniers en les additionnant ou en les soustrayant. Dommage, car ce genre d'opération peut parfois être très efficace en termes de vitesse d'exécution. A titre d'exemple examinons le balayage d'un tableau au moyen d'une boucle FOR NEXT, comme il a été présenté au chapitre précédent. Chaque itération déclenche le calcul de l'adresse d'un élément du tableau. Pour l'obtenir, on multiplie l'indice de l'élément par sa longueur et on y ajoute l'adresse de base du tableau. Or cette adresse ne diffère de la précédente que par une constante, la longueur de l'élément. Le mode de calcul est donc exagérément compliqué. Ne pourrait-on pas au démarrage de la boucle charger un pointeur avec l'adresse du premier élément du tableau et par la suite, à chaque itération, incrémenter simplement ce pointeur en lui ajoutant la longueur d'un élément ? Cette démarche ne pose aucun problème, car les tableaux de Turbo Pascal tiennent nécessairement dans un segment de mémoire unique et les procédures comme GetMem et New normalisent la forme des pointeurs renvoyés. On n'a donc pas à craindre que l'offset ne déborde le segment référencé. Dans les coulisses du langage 2-87 La Bible Turbo Pascal 2- 88 Il suffirait donc à chaque itération d'ajouter à la partie déplacement du pointeur la longueur d'un élément. Mais hélas, Turbo Pascal ne le permet pas, à moins que... Oui, c'est possible, à condition de mettre en oeuvre une petite astuce qui permet au programme d'accéder tant au segment qu'à l'offset d'un pointeur. Et ce n'est même pas difficile, car la structure d'un pointeur en Turbo Pascal est parfaitement connue et elle peut être émulée par un enregistrement: PtrRec - record [ Emule la structure d'un pointeur ).J Ofs, j Adresse de l'offset ).j Seg: W0RD [ Adresse du segment ).J end; Grâce au transtypage on peut faire croire à Turbo Pascal qu'un pointeur est structuré selon ce type d'enregistrement, et on peut ainsi accéder à son offset. Sous la forme du champ Ofs, l'adresse d'offset est considérée con-œw une valeur numérique tout à fait ordinaire, que l'on peut donc manipuler à sa guise. Pour faire avancer le pointeur sur l'élément de tableau suivant, l'instruction INC est idéale, car elle génère un code machine compact et rapide. La longueur d'un élément du tableau est renvoyée par l'opérateur SizeOf, qui peut être donc utilisé dans l'instruction INC. Le listing suivant montre une réalisation concrète de toutes ces idées. program Arithpointeur;J j-- Déclarations des types et des constantes --------------------------)J 'J type Info - record j Le tableau ).j Champi : integer;J Champ2 boolean;.J Champ3 : string[5];.J end ;.J J InfoPtr - 'Info; j Pointe sur un enregistrement Info )J J PtrRec - record j Emule la structure d'un pointeur )J Ofs, j Adresse de déplacement )J Seg : WORD; j Adresse de segment )J end; -J J Le tableau à parcourir )J j Compteur d'itérations )J Pointeur sur chaque élément )J j— Variables globales J j Pointe sur le premier élément )J j Parcourt la boucle ).J var Tablolnfo array [1. .100] 0f Info I : integer; IP : InfoPtr; J begi nJ IP :- TabloInfo[i]; for 1:-1 to 100 do begi nJ IP.Champi :- i;J IP.Champ2 : TRUE;J IP.Champ3 J Avance le pointeur sur l'élément suivant --------------- inc( PtrRec( IP ).Ofs. sizeof( Info end -,.j J end 2-88 La Bible Turbo Pascal 2-89 Dans les coulisses du langage Pour parcourir le tableau de données et pour initialiser les champs des enregistrements, on n'a pas fait appel à l'instruction WITH. En effet, le pointeur IP était déjà disponible et pouvait être utilisé tel quel pour adresser les éléments du tableau. Même si vous trouvez ce genre de construction un peu obscur, vous verrez que dans la pratique la technique mise en oeuvre peut apporter des gains de vitesse d'exécution appréciables. Le code machine généré se rapproche peu à peu du code optimal que seul l'assembleur peut procurer. Au chapitre 6 vous en trouverez un exemple dans l'unité de manipulation directe de l'écran. Cette unité fait abondamment appel aux ressources de l'arithmétique des pointeurs et atteint ainsi des performances inhabituelles pour un programme écrit en Pascal. V Les pointeurs de code Les pointeurs de données sont des objets usuels en Pascal, mais est-il vraiment nécessaire d'introduire des pointeurs de code ? S'il vous est arrivé de réaliser une application importante, vous en avez certainement éprouvé le besoin. II existe de nombreuses situations dans lesquelles il est avantageux de pouvoir appeler une fonction ou une procédure par un pointeur. On économise souvent de cette manière une clause CASE qui sert à aiguiller le programme sur la procédure ou la fonction appropriée aux circonstances. Le Pascal standard ne connaît pas cette possibilité et Turbo Pascal jouait aussi les ignorants dans ce domaine, jusqu'à la version 5.0. Jusque là, le programmeur qui voulait utiliser des pointeurs de code en était réduit à se battre avec des INLINE. Mais les choses sont maintenant différentes avec la version 5.0 qui sur ce point rapproche Turbo Pascal du langage C, ce dernier soutenant depuis toujours les pointeurs de code. Turbo Pascal désigne les pointeurs de code sous le terme de "variables de type sous-programme", ce qui concerne aussi bien les variables de type procédure que les variables de type fonction. Pour notre part, nous continuerons de parler de pointeurs de code, cette dénomination évoquant parfaitement la représentation interne de ces objets. Pour créer un pointeur de code, il faut d'abord déclarer la fonction ou la procédure concernée au moyen d'une instruction TYPE. Ainsi: type PrintFunc - function (s: string): integer ; I définit PrintFunc comme un pointeur sur une fonction qui prend comme argument une chaîne de caractères et renvoie un entier. Il est très important de communiquer avec précision les paramètres transmis et le résultat (pour une fonction) car les pointeurs de code sont toujours typés et les possibilités de transtypage de pointeurs de code sont très limitées. Lorsqu'on crée une variable qui doit jouer le rôle de pointeur de code, il faut lui associer le type sous-programme précédemment déclaré. Voici comment on définit la variable PrintPtr comme pointeur de code sur une fonction de type PrintFunc: Dans les coulisses du langage 2-89 La Bible Turbo Pascal 2-90 var PrintPtr: PrintFunc; I Par la suite, vous pourrez affecter à PrintPtr l'adresse d'une fonction quelconque, à condition évidemment qu'elle soit rigoureusement du type PrintFunc. A cet effet, Turbo Pascal ne contrôlera pas seulement les types et l'ordre des paramètres, mais aussi leurs noms qui doivent correspondre exactement à ceux de la déclaration de type. Seule une fonction écrite par exemple: function LaserPrint(s: string): integer; I obéirait à ces critères et pourrait être valablement utilisée dans l'affectation: PrintPtr:— LaserPrint; I Contrairement à ce qui se passe pour les pointeurs de données, l'opérateur @ "adresse de" ne doit pas apparaître, sinon Turbo Pascal interrompt la compilation en signalant une erreur. Il existe d'autres contraintes limitant l'appel des procédures ou des fonctions par des pointeurs de code. Elles sont essentiellement dues aux impératifs de la compilation: • la procédure/fonction doit être de type FAR, car les pointeurs de code sont eux-mêmes par nature toujours de type FAR Il suffit pour cela de mettre la directive de compilation ($F+) avant la déclaration de la procédure/fonction. Si cette règle n'est pas respectée, Turbo Pascal émet un message d'erreur "Invalid procedure reference". • la procédure/fonction ne doit pas être déclarée comme INLINE. Turbo Pascal en effet n'installe pas de sous-programme pour les séquences INLINE, mais Insère directement le code correspondant dans le programme. • la procédure/fonction ne doit pas être locale (définie à l'intérieur d'une autre), car dans ce cas elle n'est accessible que localement. Dès qu'une procédure/fonction respecte toutes ces règles, plus rien ne s'oppose à ce qu'elle puisse être appelée par un pointeur de code. Du point de vue de la syntaxe, cet appel ressemble à l'appel d'une procédure/fonction par son nom, sauf que ce n'est pas le nom de la procédure/fonction qui est donné, mais le nom du pointeur de code correspondant. En reprenant l'exemple introduit précédemment, voici à quoi ressemblerait un appel utilisant le pointeur de code PrintPtr: var resuit: integer;J begin.J resuit:— PrintPtr('test');J end; Comme au niveau interne les pointeurs de code ne se distinguent pas des pointeurs de données, ils peuvent comme n'importe quel type de données, former des tableaux 2-90 La Bible Turbo Pascal 2-91 Dans les coulisses du langage ou des enregistrements ou servir de paramètre transmis à des procédures ou des fonctions. Par contre, il est interdit de définir un pointeur de code comme constante typée. Pour une raison que j'ignore, le compilateur signale alors une erreur numéro 99 (File and Procedure Types are flot allowed hem). Les affectations ne peuvent donc être effectuées qu'au moment de l'exécution du programme. Le listing suivant documente un problème de syntaxe qui surgit lorsqu'on cherche à vérifier si un pointeur de code pointe bien sur une fonction déterminée: program Adresse_PCode;J .J type GetFon - function lnteger;J J var GetPtr : GetFon; f Pointe sur la fonction GetFon Li J (SF+) function GetOuatre : integer; ($F-).J

beginJ GetOuatre : 4.J end;J J J begin.J GetPtr :— GetOuatre; f Met l'adresse de GetOuatre dans GetPtr Li writeln( GetPtr ); ( Appelle GetOuatre par GetPtr ).i if @GetPtr - @GetOuatre then f GetPtr pointe-t-11 sur GetOuatre 1 Li writeln ('GetOuatre actif'); f Oui ).i writeln( 'GetPtr se trouve à l'adresse ', longint( @@GetPtr ));.J Ici le type GetFon est déclaré comme pointeur de code sur une fonction sans argument et renvoyant un résultat de type integer. GetPtr est définie comme une variable de ce type et chargée avec l'adresse de la fonction GetQuatre. La fonction GetQuatre est appelée par le moyen de GetPtr et son résultat est affiché par Writeln. Vous voyez que les fonctions référencées par des pointeurs peuvent très bien être appelées à l'intérieur d'une expression. Le problème n'apparaît qu'à l'instruction suivante. On se propose de tester par un IF si le pointeur de code pointe bien sur la fonction GetQuatre. On aurait tendance à écrire spontanément: if GetPtr - GetFour then Mais par définition, si on mentionne le nom d'une fonction à l'intérieur d'une expression, la fonction en question se trouve déclenchée. Donc ici GetPtr serait comparé au résultat de GetQuatre, ce qui conduirait à une erreur de compilation, puisqu'un pointeur de code n'est pas compatible avec une valeur entière. Pour permettre malgré tout ce genre de comparaison, Turbo Pascal introduit une syntaxe particulière qui spécifie que les deux opérandes, dans cette situation, doivent être précédés de l'opérateur "adresse de" @. Cette disposition permet à Turbo Pascal de reconnaître l'intention de comparaison et le contenu du pointeur de code est ainsi mis en rapport, de façon appropriée, avec l'adresse de la fonction GetQuatre. Mais à vrai dire une autre difficulté surgit aussitôt car l'expression @CetPtr doit normalement retourner l'adresse de la variable GetPtr. Turbo Pascal décide alors que Dans les coulisses du langage 2-91 La Bible Turbo Pascal 2-92 pour obtenir l'adresse d'un pointeur de code il faut employer deux préfixes @ au lieu d'un seul. La dernière instruction du programme montre un exemple d'application de cette disposition. Ces cas particuliers sont plutôt rares dans la pratique. Aussi voudrais-je vous présenter une application typique des pointeurs de code: la programmation des pilotes de périphériques ( communément appelés drivers), par exemple des pilotes d'imprimante chargés de générer les caractères appropriés pour tel ou tel type d'imprimante. La technique mise en oeuvre permet d'isoler les procédures et les fonctions de façon à ne pas les emmêler avec les propriétés particulières des différents périphériques: elles peuvent dès lors se concentrer uniquement sur leur tâche. Dans l'exemple qui suit, on définit l'enregistrement ProcFonc qui regroupe 4 pointeurs de code. Ils servent d'interfaces aux sous-programmes qui d'une part ouvrent et referment les périphériques, et d'autre part reçoivent et émettent leurs caractères. La variable globale CallPF est définie comme étant de ce type. A l'intérieur du programme principal, elle est initialisée avec les adresses des sous-programmes MyOpen, MyClose, MyGet et MyPut avant d'être transmise à la procédure CallThem. Cette dernière appelle les quatre sous-programmes au moyen des pointeurs de code qui forment les champs de l'enregistrement Ca11PF. On peut la considérer comme prototype d'une procédure qui pour communiquer avec un périphérique se servirait de diverses procédures et fonctions transmises par des pointeurs. Pour vérifier que les quatre sous-programmes My... sont réellement exécutés, un affichage approprié est prévu. program ProcPtr;J - -I type ProcFonc - recordJ Open : function : integer;.J Close: procedure( Handie : integer );J Put : procedure( DataType : integer; Pointeur : pointer );J Get : function( DataType : integer ) : pointer ;.J end ;J var Ca11PF : ProcFonc; ( Enregistrement de portée globale composé de J pointeurs sur fonctions/procédures ) J J ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ).i J ($F+) function MyOpen : integer; F-)J J beginJ writeln( Appel à la fonction I4yOpen);J MyOpen :— 3;J end ;.J

------------------------------------------------------------------- J ($F+) procedure MyClose( Handie : integer ); ($F-)-j J beginJ writeln('Appel à la procédure Close avec le N • Handle );J end ;.J J ------------------------------------------------------------------- J ($F-+-) procedure MyPut( DataType : integer; Pointeur : pointer ); ($F-).J J 2-92 La Bible Turbo Pascal 2-93 Dans les coulisses du langage begi n.J writeln('Appel à la procédure Put avec le type de données ', Datatype);.J end 'J t----------------------------------------------------------------------------- 'J ($F+) function MyGet( Dataîype integer ) pointer ; J begin,J writeln( 'Appel à la fonction Get avec le type de données ', Dataîype);.J MyGet NIL-,-j end; 'J C----------------------------------------------------------------------------- 'J procedure CallThem( var PF : ProcFonc );.J 'J var Handle : integer; t Mémorise le résultat de Open Li RetPtr : pointer; t Mémorise le résultat de Get Li 'J begi n.J Handle PF.Open; ( Appel de la fonction Open ),J C cs:01BC C47E04 les di,[bp+04] ;Charge en ES:DI un pointeur sur PF C cs:O1BF 26FF1D cail far es:[di] ;Appelle Open par un pointeur Li C ; sur PFF128M ) t cs:O1C2 8946FE mov [bp-02],ax ;Met le résultat de la fonction ).J t dans une variable locale Li -J RetPtr PF,Get( 1 ); t Appel de la fonction Get t cs:01C5 880100 mov ax,0001 ;Charge en AX le paramètre 1 Li cs:01C8 50 push ax ; puis l'empile Li C cs:01C9 C47E04 les di,[bp+04] ;Charge en ES:DI ),J t ; un pointeur sur PF Li cs:01CC 26FF5DOC cali far es:[di+OC] ;Appelle Get par un pointeur ).J C ; sur PF Li C cs:O100 8946FA mov [bp-06],ax ;Met le résultat de la fonction),i ( cs:O1D3 8956FC mov [bp-04],dx ; dans une variable locale ).i 'J PF.Put( 2, RetPTr ); t Appel de la procédure Put ).i t cs:O1D6 880200 mov ax,0002 ;Charge en AX le paramètre 2 Li C cs:O1D9 50 push ax ; puis l'empile Li t cs:01DA FF76FC push [bp-04] ;Met le contenu de RetPtr cs:0100 FF76FA push [bp-06] ; sur la pile - Li ( cs:01EO C47E04 les di.[bp+04] ;Charge en ES:DI un pointeur ).J C ; sur PF.J cs:01E3 26FF5D08 cail far es:[di+08] ;Appelle Put par PE Li -J PF.Close( Handle ); t Appel de la procédure Clode ).i t cs:O1E7 FF76FE push [bp-02] ;Empile le numéro Handle cs:01EA C47EO4 les di,[bp+04] ;Charge en ES:DI un pointeur Li f ; sur PF-1 C cs:01ED 26FF5D04 cali es:far [di+04] ;Appelle Close par PF -J end; .J -J begin.J writeln;.J with Ca11PF do t Initialise l'enregistrement Ca11PF avec Li begin t des pointeurs sur les différentes Li Open MyOpen; t fonctions et procédures Li f cs:020E B81AO0 mov ax,001A ;Déplacement Li t cs:0211 BAD05F mov dx,5FDO ; segment de Open Li t cs:0214 A33E00 mov [CALLPF.OPEN],ax ; à stocker dans un Li t cs:0217 89164000 mov [CALLPF,OPEN+2],dx ; pointeur de code Li t ; de l'enreg. Ca11PF Li 'J Close MyClose;.i C cs:021B 887800 mov ax,0078 ;Déplacement et Li t cs:021E BAD05F mov dx,5FDO ; segment de Close Li Dans les coulisses du langage 2-93 La Bible Turbo Pascal 2-94 cs:0221 A34200 mov [CALLPF.CLOSE].ax à stocker dans un cs:0224 89164400 mov [CALLPF.CL0SE+2].dx ; pointeur de code ).J ( de l'enreg. Ca11PF )J J Put :— MyPut;J C cs:0228 B8E700 mov ax.00E7 ;Déplacement et cs:022B BADOSF mov dx.5F00 ; segment de Put )J cs:022E A34600 mov [CALLPF.PUT].ax à stocker dans un )J cs:0231 89164800 mov [CALLPF.PUT+2].dx ; pointeur de code )J ( de Venreg. CallPF Li .J Get :— MyGet;J C cs:0235 B85601 mov ax.0156 ;Déplacement et Li cs:0238 BAD05F mov dx.5FDO ; segment de Get cs:023B A34A00 mov [CALLPF.GET].ax à stocker dans un Li cs:023E 89164C00 mov [CALLPF.GET+2].dx ; pointeur de code ).J C de Penreg. Ca11PF )J J end;J CallThem( Ca11PF ); C CaliThem et fonctions Li appelle les procédures writeln;J end. Il est intéressant d'observer comment à l'intérieur de l'enregistrement Ca11PF Turbo Pascal affecte les adresses des différentes procédures et fonctions aux pointeurs de code. J'avais d'abord pensé que seul l'offset serait chargé comme une constante et que le segment serait tiré du registre CS. Mais ce n'est pas exact, car Turbo Pascal charge aussi l'adresse du segment comme une constante. Cette précaution est probablement due à ce que les procédures et fonctions référencées peuvent également se trouver dans d'autres unités, et par conséquent dans d'autres segments de code que celui indiqué par CS dans le programme principal. Mais il est très étonnant que Turbo Pascal se complique la tâche en chargeant d'abord les deux adresses dans les registres AX:DX avant de les transférer dans les deux parties du pointeur de code. Le processeur aurait volontiers accepté de mémoriser directement les constantes dans le pointeur, ce qui aurait économise deux instructions. Regardez bien comment, à l'intérieur de la procédure CaliThem, Turbo Pascal appelle les procédures et fonctions par leurs pointeurs. Pour ce qui est de la transmission des paramètres et du retour du résultat, le mécanisme est usuel. Seule l'instruction CALL qui appelle les sous-programmes est inhabituelle, car l'adresse cible n'est pas une constante. Elle est issue d'un pointeur de code qui fait partie de l'enregistrement transmis sous forme de paramètre variable (c'est-à-dire de pointeur de donnée). Ainsi Turbo Pascal charge le pointeur qui pointe sur l'enregistrement dans les registres ES:DI. Les pointeurs de code peuvent donc être atteints par ES:[DI], ES:[DI+4], etc.... Pour exécuter l'instruction CALL, le processeur va chercher l'adresse FAR du sous-programme à appeler dans la mémoire ES:[DI+xx] correspondante puis il poursuit le déroulement du programme à cette adresse. En guise de conclusion, on retiendra que les pointeurs de code sont avantageux chaque fois qu'une procédure ou une fonction est construite comme un cadre général, une structure universelle à l'intérieur de laquelle les pointeurs effectuent les adaptations particulières. C'est dans ce sens que nous rencontrerons encore souvent les pointeurs de code dans la suite de cet ouvrage.

Gestion du tas

GetMem et Freemem ...............................................95 Mark et Release ..................................................102 Une nouvelle routine d'erreur pour le tas ...........................103 Le tas (en anglais heap) est une zone de la mémoire qui joue un grand rôle un Pascal. Alors que dans le segment de données les variables sont mémorisées de façon purement statique, le tas est fait pour recevoir des allocations dynamiques de variables et de tampons. Comme cette allocation n'a lieu qu'au moment de l'exécution, elle est très précieuse pour manier les structures dont la taille dépend des données et n'est connue qu'à l'exécution du programme. Réaliser des arbres, des listes chaînées ou des graphes sans l'appui du tas relève de la gageure. Cette section est destinée à vous montrer quelques-uns des mécanismes qui commandent la gestion de cet espace mémoire. Par défaut, Turbo Pascal réservepour le tas toute la mémoire comprise entre la fin du segment de données et la fin de la mémoire vive (jusqu'à la frontière des 640 Ko). Si cette quantité est excessive pour vos besoins, vous pouvez la changer au moyen de l'option "Compiler/Memory Size" du menu "Options" ou en installant la directive de compilation ($M). De la même manière vous pouvez aussi fixer une taille minimale pour le tas si au moment de la compilation vous êtes en mesure de la prévoir. Au niveau du système d'exploitation, le loader EXEC ne chargera le programme que si vos exigences peuvent être satisfaites. Si la mémoire est insuffisante, le démarrage du programme sera interrompu avec émission d'un message d'erreur approprié. Pour gérer le tas au niveau interne, Turbo Pascal se sert de trois pointeurs appelés HeapOrg, HeapPtr et FreePtr et qui sont déclarés au sein de l'unité System. C'est à cet endroit qu'ils sont initialisés avant même le début du programme. Tout au long de l'exécution du programme, HeapOrg pointe sur le début du tas. Le contenu de HeapPtr est plus changeant, car il pointe sur la fin du tas alloué et suit donc les mouvements d'allocation et de désallocation des blocs mémoire. Le pointeur FreePtr a lui aussi la bougeotte car il référence la liste des fragments dont nous allons parler par la suite.

Cet attelage à trois chevaux est commandé par les différentes procédures et fonctions que Turbo Pascal propose pour accéder au tas. Ces fonctions peuvent être réparties en deux groupes. D'un côté on peut ranger les ordres Mark et Release, qui permettent d'allouer et de libérer la mémoire d'une façon simple et brute. De l'autre côté on trouve les procédures GetMem et FreeMem, ainsi que New et Dispose. Comme le montrera le chapitre 2.7, New et Dispose font eux-mêmes appel à GetMem et FreeMem. Le cercle des candidats se réduit donc aux couples Mark et Release d'une part, et GetMem et FreeMem d'autre part. Nous allons d'abord nous intéresser au fonctionnement de ces deux dernières procédures.

GetMem et FreeMem

Examinons ce qui se passe au premier appel de GetMem après le lancement du programme. GetMem doit d'abord vérifier s'il reste encore assez de place entre la fin du tas alloué et la fin de l'espace global attribué au tas. En général, il restera de la place Dans les coulisses du langage 2-95 La Bible Turbo Pascal 2-96 disponible car à ce moment-là aucune mémoire n'a encore été prise sur le tas. Mais si le programme est important et si la mémoire vive est réduite, le premier essai d'allocation peut déjà échouer. Turbo Pascal met alors fin au programme en affichant un message d'erreur. Nous supposons que tel n'est pas le cas. L'adresse du début de la mémoire disponible est contenue dans HeapPtr qui à l'origine pointe sur le début du tas et est identique à HeapOrg. En même temps qu'elle reporte la valeur de HeapPtr dans le pointeur transmis comme paramètre, la procédure GetMem augmente HeapPtr du nombre d'octets réservés. HeapPtr pointe donc sur un emplacement situé au-delà de la mémoire allouée, ce qui garantit que cette dernière ne sera pas attribuée une nouvelle fois au prochain appel de GetMem. Après cette opération, HeapPtr est normalisé: l'adresse du segment est ajustée de manière que le déplacement soit compris entre O et 15. La même règle s'applique au pointeur renvoyé au programme appelant. Théoriquement un bloc de mémoire allouée peut occuper un segment entier de 64Ko, mais la normalisation fait perdre une petite marge de 15 octets. Avec GetMem, on peut donc allouer des blocs d'une taille maximale de 65521 octets (65536-15). Si d'autres appels à GetMem succèdent au premier, le contenu courant de HeapPtr est à chaque fois renvoyé à l'appelant et le pointeur est avancé au-delà de l'espace alloué. En conséquence, les blocs alloués se trouvent les uns à la suite des autres et leur ensemble croît en avançant vers la fin du tas. Heap vide GetMem (pi. 1000); GetMem (p2,75); FreeMem (p2,75); FreeMem (p1, 1000); Fin de la Mémoke *0





I—, He ____ F1 KeapPV ____ P2 - HeapPU -'fl ______ P1 1000 1000 Pl 1000 ____ j—,Heap .sLt He* Org occupé D libre Figure 24 : Allocation du tas par GetMem et FreeMem Si les blocs alloués sont libérés par FreeMem dans l'ordre inverse de leur allocation, le tas est facile à gérer car HeapPtr se rapproche de son début par simple décrémentation de l'espace à libérer. Mais ce cas de figure idéal est rare en pratique. Il arrive fréquemment qu'un bloc soit libéré au milieu du tas. Il en résulte une sorte 2-96 La Bible Turbo Pascal 2-97 Dans les coulisses du langage de "trou" dans le tas, c'est-à-dire un fragment de mémoire inutilisé. Par la suite, GetMem devra tenir compte de ces trous, sinon leur nombre risque d'augmenter sans cesse alors qu'à chaque allocation le pointeur HeapPtr se déplace vers la fin du tas. En fin de compte, on aboutirait à une situation où la fin du tas serait atteinte : il s'avérerait impossible d'allouer de la mémoire supplémentaire, alors même qu'il subsiste de nombreux trous inoccupés. Turbo Pascal résout ce problème en gérant une liste de fragments, dans laquelle sont consignées les adresses de début et de fin de chaque fragment de mémoire inutilisé (très précisément : l'adresse de "fin" est celle du premier octet qui suit le fragment). Chaque fois qu'un bloc libéré par FreeMem se trouve au milieu du tas, Turbo Pascal reporte ce fragment dans la liste. Si un bloc contigu à un fragment, ou encadré par des fragments, se trouve libéré, il n'est pas catalogue comme un nouveau fragment, mais la liste des fragments est actualisée en conséquence. S'il existe des fragments, Turbo Pascal consulte leur liste à chaque appel de GetMem pour vérifier s'il ne s'y trouve pas un espace capable de satisfaire la requête de l'appelant. Dans le meilleur des cas, GetMem trouve un fragment qui a juste la taille souhaitée. Le trou disparaît ainsi et la liste des fragments s'appauvrit d'un élément. Mais cette situation est plutôt rare, et GetMem se satisfait également de fragments qui sont plus grands que l'espace demandé. Dans ce cas, le fragment subsiste mais il devient plus petit. Avec le temps, si les GetMem se multiplient, on peut espérer réduire de plus en plus les trous, et peut-être même les supprimer complètement. Turbo Pascal commence par construire la liste des fragments à la fin de la mémoire attribuée au tas: chaque fois qu'un élément vient se rajouter à la liste, cette dernière se rapproche de la fin du tas alloué. Comme il a déjà été mentionné, chaque entrée de la liste contient deux pointeurs et prend donc 8 octets. Comme Turbo Pascal limite la liste à un segment, le nombre maximal de fragments gérés est théoriquement de 8192 (65536/8). En fait l'une des entrées de la liste sert à noter l'absence éventuelle de fragments. Finalement le nombre maximal de fragments est donc de 8191. Le début de la liste des fragments est référencé par le pointeur FreePtr qui n'est pas normalisé. L'adresse de son segment est toujours l'adresse de base du segment de 64 Ko se trouvant à la fin de la mémoire attribuée au tas. Comme cet endroit est aussi la fin de la mémoire réservée au programme, l'adresse du segment de FreePtr + $1000 est celle du premier octet libre se trouvant au-delà du programme Pascal en mémoire. Le déplacement de FreePtr indique le début de la liste des fragments à l'intérieur de ce segment. Tant qu'il n'existe pas de fragment, Turbo Pascal fixe le déplacement à 0. Ce n'est qu'au moment de la création de la première entrée que le déplacement est chargé avec l'adresse du début de la liste, qui est alors de 65528. Cette valeur diminue de 8 à chaque nouvelle entrée, avançant progressivement en direction du début du segment. La gestion inhabituelle de ce pointeur est due au grand nombre d'accès que Turbo Pascal doit effectuer sur la liste des fragments lorsque GetMem ou FreeMem sont appelés. Pour accélérer au maximum les recherches, effacements et déplacements d'entrées de cette liste, Turbo Pascal n'utilise même pas les pointeurs FAR. Il mémorise en DS le début du segment enregistré dans FreePtr, ce qui lui permet de mettre en service des pointeurs NEAR pour accéder le plus rapidement possible aux fragments. Dans les coulisses du langage 2-97 La Bible Turbo Pascal 2-98 Figure 25: Fragmentallon du tas Il peut se produire une erreur critique au moment de la libération de blocs de mémoire. Turbo Pascal peut être obligé de créer une nouvelle entrée dans la liste des fragments alorsqu'il ne reste plus assez de mémoire parce que la fin du tas alloué touche le début de la liste des fragments. Pour éviter cette situation, le constante typée FreeMin de l'unité System mémorise la quantité minimale de mémoire qui doit rester comprise entre la fin du tas alloué et le début de la liste des fragments. Si cette limite est dépassée, GetMem interrompt l'exécution du programme en signalant l'erreur. En fait Turbo Pascal initialise cette variable avec la valeur 0, de sorte que dans un premier temps elle n'agit pas sur GetMem et FreeMem. Son utilisation n'est intéressante que si vous prévoyez de la part de votre programme une fragmentation importante du tas conjuguée avec de volumineuses allocations dynamiques. Si vous voulez être certain de pouvoir créer au moins 1000 entrées dans la liste des fragments, la variable FreeMin doit être fixée à 8000, pour que 8000 octets puissent rester libres entre la fin du tas alloué et la fin de la mémoire totale attribuée au tas. Le programme suivant montre comment gérer le tas au moyen des trois pointeurs HeapOrg, HeapPtr et FreePtr tout en tenant compte de la liste des fragments. Il commence par allouer au moyen de GetMem 5 blocs de 256 octets, puis il les libère à nouveau de façon à provoquer une fragmentation. L'écran affiche l'évolution des différents pointeurs et de la liste des fragments. L'accès à la liste des fragments s'opère au moyen de divers types définis par l'utilisateur et qui émulent sa structure interne: • le type fragentree décrit une entrée de la liste des fragments sous forme des deux pointeurs Debut et Fin 2-98 La Bible Turbo Pascal 2-99 Dans les coulisses du langage • la liste des fragments est représentée par le type fragtablo qui est un tableau de 8191 entrées du type fragentree le type fragptr permet de référencer le tableau En transtypant le pointeur FreePtr qui est par nature dépourvu de type, on le transforme en un pointeur de type fragptr. Le programme peut alors accéder à la liste des fragments comme il accéderait à n'importe quelle autre tableau de Pascal. Cette faculté est abondamment mise à profit par l'affichage des entrées.

  • T A S : montre la gestion du tas au moyen des pointeurs
  • Heaporg. HeapPtr et FreeLlst *J
    • ---------------------------------------------------------------------------
  • Auteur MICHAEL TISCHER
  • Développé le : 1.08.1989
  • Dernière MAJ : 1.08.1989

J program tas-.-j J uses Crt;.J J (-- Déclarations de types ---------------------------------------------------- j type hstring - string[4]; ( Types chaînes de caractères hpstring - string[9]; ( pour HexStr et HexPtr j j fragentree - record ( Emule une entrée de Debut, t la liste des fragments Li Fin pointer:.J end;.J fragtablo - array [i. .8191] 0f fragentree; ( Liste des fragments )_i fragptr - fragtablo; t Pointe sur la liste des fragments U j C-- Variables globales ------------------------------------------------------- j var pi, p2. p3. p4. p5 : pointer; ( Pointeurs sur les zones d'expérimentation ).J 'J

  • GetFrags: donne le nombre d'entrées de la liste des fragments
    • ---------------------------------------------------------------------------
  • Entrée : néant
  • Sortie : Nombre de fragments

j function getfrags : integer;J J begin.J If C ofs( freeptr ) - O ) then ( Déplacement de FreePtr - 0 7 Li getfrags :— O ( Oui, pas d'entrée dans la liste )J elseJ getfrags :— (65536 - ofs( freeptr^ )) shr 3; ( Nombre d'entrées ).J end ;J j

  • HexStr : convertit un nombre en chaîne hexadécimale
    • ---------------------------------------------------------------------------
  • Entrée HEX - nombre à convertir
  • Sortie : Chaîne représentant un nombre hexadécimal *j

j j function HexStr( hex word ) hstring;j j Dans les coulisses du langage 2-99 La Bible Turbo Pascal 2-100 var hstr : hstring; E Sert à construire la chaîne hexa ).J d. f Valeur d'un chiffre hexa )J b : byte; f Compteur d'itérations ).J J beginj hstr ' ' ; f Initialise la chaîne )J for b 1 to 4 do f Passe en revue les 4 chiffres hexa )J beginJ d :- byte(hex) and 15; f Valeur des 4 bits inférieurs )J if f d - 10 ) then f Est-ce une lettre ? ).J hstr :- chr( ord('A')-10+d ) + hstr ( Oui ).i else f Non c'est un chiffre ).J hstr :- chr( ord('O')+d ) + hstr;.J hex :- hex shr 4; f Décale à droite les 4 bits )..i end:,J HexStr :- hstr; f Retourne la chaîne ).J end;J J

  • HexPtr : retourne un pointeur en notation hexadécimale *J
    • ---------------------------------------------------------------------------
  • Entrée : P - Pointeur à traiter
  • Sortie : Chaîne contenant le pointeur en notation hexadécimale *J
                                                                                                                                                            • ) •j

-J function HexPtr( p : pointer ) : hpstring;.J -J var hp hpstring;J J begln.J hp :- HexStr( seg( p^ Convertit le segment en hexa ).J hp :—hp+':';.J hp :- hp + HexStr( ofs( p^ Convertit le déplacement en hexa ),.J HexPtr :- hp; f Retourne la chaîne )J end;.J J

  • Wait : Demande à l'opérateur de taper une touche et attend la frappe
    • ---------------------------------------------------------------------------
  • Entrée : néant *j
                                                                                                                                                            • ) J

J procedure wait;J J var ch : char;.J J begin.J write( 'Appuyez sur une touche SVP ...');-j repeat until keypressed;.J ch :- ReadKEy;.J if ch - #0 then f Tient compte du code étendu )J ch :- ReadKey;.J write( #13 ); f Revient en début de ligne ).J ClrEol: f et efface toute la ligne )J end;.-i -J

  • HeapStatus Affiche le pointeur HeapPtr et la liste des fragments *j
    • ---------------------------------------------------------------------------
  • Entrée néant *,j

j 'J procedure HeapStatus:J -J var i, f Compteur d'itérations 3J frags integer; f Nombre de fragments 3J fDebut, E Adresse de début d'un fragment )J fFin pointer; ( Adresse de fin d'un fragment )J J beginJ 2-100 La Bible Turbo Pascal 2- 101 Dans les coulisses du langage writeln( 'HeapPtr pointe sur ' . HexPtr(HeapPtr) );.J frags :- getfrags;.J writeln( 'Liste des fragments ' , frags. ' entrées');.J 1f ( frags O ) then.J for I :- 1 to frags do.J beginJ fOebut :- fragptr(freeptr)'[i].Debut;.J fFin :- fragptr(freeptr)[i].Fin;.J writeln( 'Entrée N ', I. :ll,.J HexPtr(fDebut), ' - ' , HexPtr(fFin). (longint(seg(fFin )) shl 4 + ofs(fFin )) -.J (longint(seg(fDebut)) shl 4 + ofs(fDebut)), ' Octets)' );,J end-,-j writeln-,.j end,J 'J

  • PROGRAMME PRINCIPAL *,J

'-J begin.J Cl rScr ;.J writeln( 'Le tas commence en ' , HexPtr(HeapOrg) );J writeln( 'Le tas finit en • ..J HexStr(Seg(FreePtr)+SiOOO) , ':0000');-j writeln( 'Le tas comprend longint(Seg(FreePtr)+S1000-Seg(HeapOrg)) shl 4, octets');.J HeapStatus ;,J writeln( 'GetMem va allouer 5 blocs de 256 octets chacun ');.J wait ; J getmem( pi. 256 );,J writeln( 'Bloc 1 , HexPtr( pi HeapStatus ;J wait;.J getmem( p2. 256 );.J writeln( 'Bloc 2 ' , HexPtr( p2 ) );.J HeapStatus ;,J wa I t; getmem( p3. 256 );,J writeln( 'Bloc 3 ' . HexPtr( p3 ) );.J HeapStatus ;.J wait;.J getmem( p4. 256 );_J writeln( 'Bloc 4 ' , HexPtr( p4 ) );.J HeapStatus ;.J wa I t; getmem( p5, 256 );.J writeln( 'Bloc 5 ' , HexPtr( p5 ) );.J F1eapStatus ;,J 'J writeln( 'Le bloc 4 va être effacé, il apparaît un trou entre les blocs '-+-.J '3 et 5.' );,J wait ; J il apparaît un trou entre les blocs '-s-,J freemem( p4, 256 );,J Turbo Pascal réunit alors les deux '+,J HeapStatus ;,J Turbo Pascal peut alors remonter la '+J writeln( 'Le bloc 2 va être effacé, '1 et 3,' );,J walt ;,J freemem( p2, 256 );J HeapStatus ;,J writeln( 'Le bloc 1 va être effacé, 'premiers trous,' wait ;,J freemem( pi. 256 );,J HeapStatus ;,J writeln( 'Le bloc 5 va être effacé, fin du tas.' wait -..j

freemem( p5. 256 );.J	

HeapStatus ;J Dans les coulisses du langage 2-101 La Bible Turbo Pascal 2-102 writeln( 'Le bloc 3 va être effacé, ce qui libère à nouveau le tas.' );.-i walt ;J freemem( p3. 256 );J HeapStatus ;.i wait;.J Cl rScr;.J end.

Mark et Release

Les procédures Mark et Release gèrent le tas d'une manière sensiblement moins sophistiquée. Elles influencent directement le pointeur HeapPtr. La tâche de Mark consiste à renvoyer le contenu courant de HeapPtr au programme appelant qui apprend ainsi où se trouve la fin du tas alloué. Ce pointeur, ou un autre, pourra ensuite être transmis à Release. Cette dernière procédure charge le pointeur transmis dans HeapPtr et efface en même temps la liste des fragments, de sorte que tous les trous disparaissent (on pourrait dire: sont "raccommodés"). L'utilisation de Mark et Release en conjonction avec GetMem et FreeMem peut avoir des conséquences désastreuses. Elle n'est recommandée que lorsqu'un espace mémoire important a été alloué par GetMem et que l'on est sûr que le tas n'était pas fragmenté au préalable, de sorte que les blocs alloués sont contigus. On peut alors éviter de libérer l'espace alloué bloc par bloc au moyen de FreeMem et adopter la tactique suivante: • Mémoriser par Mark le contenu de HeapPtr avant le premier appel à GetMem • Redonner à HeapPtr sa valeur initiale en utilisant Release, ce qui conduit à libérer la mémoire allouée dans l'intervalle Release peut aussi servir à effacer l'intégralité du tas. Il suffit de transmettre comme argument à la procédure un pointeur qui pointe sur le début du tas. Cette adresse peut être connue grâce à la variable HeapOrg. GetMem (p2,2000); GetMam (pl, 1000); Mark (p); GetMem (p3500); Release (p) GetMem (p41000); .4— Heap PD P4-* 1000 500

2000

p1---»~ 1000 F4apg Figure 26 : Fonctionnement de Mark et Release 2-102 La Bible Turbo Pascal 2-103 Dans les coulisses du langage Avant de recourir à ces méthodes expéditives, vous devrez vous rappeler que votre programme n'est pas tout seul à se servir du tas : les unités incorporées en font également usage et il ne faut pas les priver brutalement des variables qu'elles y ont stockées. C'est ainsi que l'unité Graph, par exemple, charge les pilotes graphiques, les jeux de caractères et des tableaux internes dans des tampons alloués sur le tas. La désallocation de ces tampons et leur écrasement par d'autres variables conduit inévitablement au plantage de votre programme. V Une nouvelle routine d'erreur pour le tas On attend généralement des applications professionnelles qu'elles ne présentent pas, en plein milieu d'une opération, un message du genre 'heap overflow error" qui laisse l'utilisateur désemparé et risque de lui faire perdre des données. Ce scénario n'est pas du tout improbable si votre programme fait un grand usage du tas, s'il est tant soit peu volumineux et si la mémoire vive est encombrée de divers pilotes et programmes résidents. Pour permettre à votre programme de conserver sa dignité dans ces moments difficiles, Turbo Pascal vous offre la possibilité, grâce à la variable HeapError, d'installer votre propre gestionnaire d'erreurs. HeapError étant un pointeur de code, il suffit de lui affecter, au début du programme, l'adresse de votre routine. Cette routine ne peut pas être de type quelconque mais doit être déclarée sous la forme: I function HeapError(Size: Word): Integer; I Comme toutes les procédures et fonctions appelées par l'intermédiaire d'un pointeur, HeapError doit être de type FAR, ce qui peut notamment se commander par la directive de compilation [$F+). La fonction est appelée lorsqu'un besoin en mémoire n'a pas pu être satisfait par GetMem. La taille du bloc à allouer est alors transmise comme paramètre Size. Le résultat de la fonction se présente sous la forme d'un nombre qui indique à Turbo Pascal ce qu'il doit faire: O: provoque une erreur d'exécution et interrompt le programme 1: le programme n'est pas interrompu, mais la procédure appelée (New ou GetMem) reçoit comme paramètre le pointeur NIL, ce qui lui permet par une programmation adéquate de traiter le cas particulier qui se présente 2: Turbo Pascal essaye encore une fois d'allouer la mémoire demandée. En général ce deuxième essai donne le même résultat que le premier, de sorte que la fonction est à nouveau invoquée. Du point de vue de Turbo Pascal, la fonction appelée par HeapError est une fonction Pascal tout à fait ordinaire sans attribut spécial. L'utilisateur peut donc programmer une sauvegarde automatique des données, afficher des avertissements, des conseils, etc.. Dans tous les cas, la routine vaudra sans doute mieux qu'une interruption abrupte et pour la plupart des opérateurs totalement incompréhensible. Dans les coulisses du langage 2-103 La Bible Turbo Pascal 2- 104 Programm Unité SYSTEM HeapError := @MyHeapError GetMem(p10000); HeapError (10000); ($f+} function MyHeapError (Size Word) Integer; Iin yHeapError 1; end; Figure 27: Appel dune routine d'erreur par HeapError

L'unité System

Abs.............................................................105 Addr, opérateur ©.................................................106 Chr.............................................................108 CSeg, DSeg, SSeg, SPtr ............................................109 Dec, Inc .........................................................109 Exit.............................................................110 Hi,Lo ...........................................................111 Length..........................................................111 Odd.............................................................112 Seg,Ofs .........................................................113 Ord.............................................................115 pj...................................115 Pred,Succ .......................................................116 Ptr..............................................................117 SizeOf...........................................................118 Write et Writeln ..................................................118 Read et Readin ...................................................122 New, Dispose et consorts ..........................................122 2-104 La Bible Turbo Pascal 2-105 Dans les coulisses du langage

L'unité System n'est pas simplement une unité parmi d'autres, elle présente des caractères particuliers. En témoigne d'ailleurs son inclusion automatique sans instruction USES explicite. Mais il existe d'autres raisons qui font que l'unité System est pour Turbo Pascal plus qu'une collection de procédures et de fonctions librement appelables.

II semble même que cette unité ait une part d'intelligence propre qui lui permet par exemple d'appliquer une même fonction à des types de données tout à fait différents ou de générer du code spécifique selon le type de données traité. Même en version 5.0 (et 4.0) Turbo Pascal est déjà un langage orienté objet en ce sens qu'il tient compte au moment de la compilation du type d'argument transmis à une fonction ou à une procédure.

Il est également intéressant de remarquer que de nombreuses fonctions ont été conçues en assembleur et qu'elles ne sont pas appelées sous forme de sous-programmes. Turbo Pascal traduit les appels correspondants en séquence d'ordres machine, œ qui rappelle les procédures Inline qui insèrent directement du code assembleur. Ces séquences dépendent du type et même de la valeur des paramètres transmis, dans la mesure où ils sont connus au moment de la compilation. Il s'avère finalement que l'unité System est plus qu'une unité au sens propre du terme, elle est une composante fixe du compilateur. Comment expliquer autrement que des mécanismes d'optimisation y aient été implémentés alors qu'ils font défaut aux autres unités?

Le tableau suivant liste les fonctions, procédures et références de variables de l'unité System que Turbo Pascal à chaque appel transforme directement en séquences d'assembleur: Abs Dseg Length Pred SSeg Addr Exit Odd Ptr Suce Chr Hi Ofs Seg CSeg Inc Ord SizeOf Dec Lo PI SPtr

Dans les pages qui suivent je voudrais vous présenter les différentes fonctions et leurs séquences de transformation. En règle générale, chaque fonction sera décrite par plusieurs appels mettant en jeu divers paramètres et diverses affectations du résultat. Si les arguments de la fonction sont des variables, ces dernières ne seront pas initialisées par le programme, de sorte que l'appel de la fonction à l'intérieur d'un programme réel est dénué de sens. Mais comme il s'agit ici de programmes de démonstration, nous ne nous arrêterons pas sur ce problème.

Abs

La fonction Abs renvoie la valeur absolue (= positive) de l'argument transmis, qui doit nécessairement appartenir à la classe des entiers (Byte, Shortint, Word, Longlnt) ou des réels. Si Turbo Pascal peut connaître la valeur de l'expression au moment de la compilation, il s'agit alors d'une constante au sens général du terme. Sa valeur absolue est déterminée à la compilation et affectée à la variable appropriée par un ordre MOV. A cette occasion, Turbo Pascal est capable de déceler le franchissement éventuel de l'intervalle de définition. Le programme qui suit essaye d'attribuer la valeur absolue de 300 à une variable de type Byte. Turbo Pascal réagit en interrompant la compilation et en affichant un message d'erreur.

Si l'expression ne peut être évaluée qu'au moment de l'exécution du programme, Turbo Pascal génère les instructions capables de la calculer et teste ensuite si le résultat est positif ou négatif. Dans ce dernier cas, le résultat est transformé en son opposé par l'ordre NEG de façon à devenir positif. La manipulation correspondante ne peut être effectuée directement que si l'expression est entière. Pour établir la valeur absolue d'un résultat en virgule flottante, le compilateur fait appel à un sous-programme car le calcul est trop complexe et trop long pour être incorporé sous forme de code machine. C—ABS: à étudier le code machine Programme de démonstration servant généré par la fonction ABS de Turbo Pascal J program C.A8S.J J var r : real ;J 1 : longint;.J I : lnteger;.J b : byte;J J begin.J i :— Abs( 1 * 4 );.J C cs:0008 A14800 mov ax,[I] ;Charger I en AX cs:000B B90200 mov cx,0002 ;Mettre 2 dans le compteur de )J décalages C cs:000E 03EO shl ax,cl :AX :— AX * 4 cs:0010 09Go or ax.ax ;Combine AX avec lui-même cs:0012 7902 jns 0016 ;AX ( 0 1 NON cs:0014 F708 neg ax ;OUI, calcule l'opposé cs:0016 A24A00 mov [I],ax ;Note le résultat < J b :— Abs( -5 );J cs:0019 C6064A0005 mov byte ptr [B].05 ;Constante I

:— Abs( 32767 );J f cs:001E C7064800FF7F mov word ptr [I].7FFF idem '-I :— Abs( -1000000 );.J ( cs:0024 C70644004042 mov word ptr [L].4240 ;Charge le mot de )J ( poids-faible ).-i E cs:002A C70646000F00 mov word ptr [L+2].000F ;Charge le mot de ).J poids fort r :— Abs( pj * 2.0 );J C cs:0030 C7063E008321 mov word ptr [R].2183 ;Charge une constante).J cs:0036 C7064000A2DA mov word ptr [0040].DAA2 ;réelle sur 6 octets )J f cs:003C C70642000F49 mov word ptr [0042].490F J (Sifdef ERREUR ).J b :— Abs( 3.0 ); C ERREUR: Type mismatch )J b :— Abs( 300 ); f ERREUR: Constant out 0f Range )J Abs( 322721 ); C ERREUR: Constant out 0f Range )J (Sendi f)J J

Addr, opérateur @

Comme l'opérateur @, la fonction Addr sert à connaître l'adresse d'une variable, d'une constante typée ou d'une procédure/ fonction. Indépendamment du type de l'argument, le compilateur charge l'adresse comme pointeur FAR dans les registres DX:A)( et de là il l'affecte à une variable ou la dépose sur la pile si elle doit être transmise comme paramètre de fonction ou de procédure. Cette suite d'opérations n'est pas très performante quand le compilateur connaît déjà par ailleurs l'adresse de la variable, ce qui est certainement le cas des variables globales et absolues. Le segment et le déplacement de la variable pourraient alors être affectés sans l'intermédiaire des registres A)( et DX.

Quand on indique dans la fonction Addr une variable absolue ou une variable adressée par des pointeurs, Turbo Pascal copie aussi le segment et le déplacement de la variable dans les registres ES et DI. La signification de cette copie n'est pas très claire, mais il est probable que le compilateur se prépare ainsi à des accès ultérieurs à la variable.

Les instructions générées par la fonction Addr montrent bien la différence entre les variables globales déposées dans le segment des données et les variables locales temporairement stockées sur la pile. Dans le premier cas, le segment est le contenu du registre DS, alors que les variables locales sont dans le segment indiqué par le registre SS.

Une instruction telle que

p:— Addr(p'); I

est dépourvue d'intérêt car P s'affecte son propre contenu. Le code généré par Turbo Pascal confirme ce cercle vicieux. Le contenu de P est chargé dans DX:AX (par l'intermédiaire de ES et DI) et de là il est à nouveau transféré dans P. On ne peut pas demander au compilateur d'éliminer ce genre d'absurdité, car elle ne devrait pas figurer dans un programme digne de ce nom. Programme de démonstration servant à étudier le code machine généré par la fonction ADDR de Turbo Pascal -J -J program C_AODR;.J -J type tamp - array[ 1. .100 1 0f BYTE;, bp - tamp;.J 1 var tampptr : bp:.J globtamp : tamp:.J P : pointer;J blosjlag : byte absolute $0040:$0049;.J .J procedure test;J

var loctamp : tarTlp;.J

begin.J tampptr :— Addr( loctamp );.J cs:000E 80469G lea ax.[bp-64] ;Déplacement relatif à BP ).J cs:0011 8CO2 mov dx.ss ;Segment - celui de la pile ).J cs:0013 A33E00 mov [TAMPPTR].ax :Dépose dans TAMPPTR le ).J cs:0016 89164000 mov [TAMPPTR+2.dx ; segment et le déplacement ).J -J tampptr :— Addr(globtamp[46]);J C cs:001À B86F00 mov ax.offset globtamp + 46; Déplacement cs:0010 8CDA mov dx.ds ;La variable est dans le 1.J Dans les coulisses du langage 2-107 La Bible Turbo Pascal 2- 108 segment de données cs:001F A33E00 mov [TAMPPTR].ax :Mémorise le pointeur FAR ).j cs:0022 89164000 mov [TAMPPTR+2].dx dans TAMPPTR

p Addr( biosflag );.J cs:0026 BE4000 mov si .0040 ;Charge le segment en SI ).i cs:0029 8EC6 mov es.si et le copie en ES (7??) )_J cs:0028 884900 mov ax.0049 ;Charge le déplacement en AX ).J cs:002E 8CC2 mov dx.es ;Transfère le segment en DX ).j f cs:0030 A3A600 mov [P],ax ;Mémorise DX:AX en P f cs:0033 8916A800 mov [P+2],dx.J

p addr( tampptr cs:0037 C43E3E00 les dI.[TAMPPTR] ;Charge TAMPPTR dans ES:DI ).J f cs;003B 89F8 mov ax.dl ;Déplacement en AX f cs:003D 8CC2 mov dx,es ;Segment en DX cs:003F A3A600 mov [P].ax ;et le tout en P f cs:0042 8916A800 mov [P+2].dx 1-1

p :- addr( p );J ( cs:0037 C43E3E00 les di.[P] ;Contenu de P en ES:DI f cs:003B 89F8 mov ax.di ;Déplacement en AX ).J ( cs:003D 8CC2 mov dx.es ;Segment en DX cs:003F A3A600 mov [P].ax :et le tout en P cs:0042 8916A800 mov [P+2).dx ;. .tout est comme avanti ).j .J end;-]

begin.J test; .J end.

Chr

Lorsque des programmeurs en langage C touchent pour la première fois à Pascal, ils sont toujours désagréablement surpris par la stricte séparation entre les types CHAR et INTEGER qui n'existe pas en C. Après tout, les types CHAR et BYTE ne sont-ils pas l'un et l'autre stockés dans un octet, alors pourquoi est-il interdit de procéder à des affectations réciproques? Bien que rien ne s'y oppose sur le plan de la technique de programmation, un tel mélange serait contraire à la philosophie de Pascal. On dirait presque de l'apartheid, mais heureusement Turbo Pascal ouvre quand même quelques possibilités de communication. L'une d'elles est la fonction Chr qui convertit une expression entière en un opérande de type Char. Du point de vue interne, les transformations nécessaires sont tout à fait minimes comme le montre le programme suivant qui affecte le code ASCII 12 à une variable de type Char. La valeur 12 est ici une constante entièrement résolue au moment de la compilation. Par conséquent, le code est directement chargé dans la variable. Il est difficile de faire plus efficace. C_CHR: Programme de démonstration servant à étudier le code machine ).j généré par la fonction CHR de Turbo Pascal -J -J progran C_CHR;.J -J var c char;d J beginJ c chr( 12 );J cs:0008 C6063E000C mov byte ptr [C].00 ;Constante! end. 2-108 La Bible Turbo Pascal 2- 109 Dans les coulisses du langage V CSeg, DSeg, SSeg et SPtr Avec ces variables on peut connaître à tout instant les contenus des registres CS, DS, SS et SP. On peut ainsi exploiter l'adresse du segment du programme principal ou d'une unité, l'adresse du segment des variables ou celle du segment de la pile. Le code généré par Turbo Pascal n'est pas toujours optimal: c'est ce que montre le listing suivant. Bien que le processeur supporte le transfert direct d'un registre de segment dans une mémoire (et donc dans une variable), le compilateur fait systématiquement un détour par le registre AX. Cette manière de procéder ne s'explique que par la volonté de séparer les affectations des variables des calculs d'expressions. Turbo Pascal considère qu'il s'agit là de deux choses complètement différentes. C'est ainsi qu'à l'issue de la détermination d'une expression, le compilateur veille toujours à disposer le résultat, selon son type, dans l'un des registres AL, AX ou dans la paire DX:AX. L'affectation du résultat survient dans un deuxième temps à l'aide de l'instruction MOV. C_SEGS: Programme de démonstration pour étudier le code machine généré par l'accès aux variables CSEG. DSEG. SSEG et SPTR )J -J program C_SEGS;.J -J var w : WORO;.J J begi n.J W : CSeg;.J C cs:0008 8CC8 mov ax.cs ;Copie CS en AX cs:000A A33E00 mov [W].ax et de là dans la variable W ).J J w DSeg;.J cs:000D 8CD8 mov ax.ds ;Copie OS en AX cs:000F A33E00 mov [W].ax et de là dans la variable W ).J -J w : SSeg;.J { cs:0012 8C00 mov ax.ss ;Copie SS en AX IJ cs:0014 A33E00 mov [W].ax et de là dans la variable W ).J -J w SPtr;.J { cs:0017 89E0 mov ax.sp ;Copie SP en AX cs:0019 A33E00 mov [W].ax et de là dans la variabke W ).J end. V Dec, Inc Les procédures Inc et Dec ne sont pas non plus traduites en appels à des sous-programmes mais elles sont directement transposées en instructions élémentaires (parfois une seule suffit). Il est donc bien plus efficace d'utiliser Inc que d'écrire une affectation du genre: X: x+1; I L'avantage en vitesse d'exécution est considérable. Car dans l'affectation Turbo Pascal charge d'abord le contenu de X dans l'un des registres du processeur, incrémente ce registre, puis renvoie le contenu du registre dans X. Dans les coulisses du langage 2-109 La Bible Turbo Pascal 2-110 La mise en service de DEC et INC évite le détour par le registre du processeur. Turbo Pascal exploite les instructions machine de même nom INC et DEC qui sont destinées à incrémenter ou décrémenter les mémoires ou les registres. Ces instructions interviennent systématiquement quand une variable doit être incrémentée ou décrémentée de 1 et qu'il s'agit d'un type représenté dans le format interne BYTE ou WORD. Les types plus longs, par exemple Longlnt, sont modifiés par la combinaison des instructions ADD et ADC. Ces mêmes instructions sont également utilisées dans tous les cas où l'incrémentation ou la décrémentation porte sur une valeur supérieure àl. C_INCbÈC:Piiiramme de démonstration pour étudier le code machine généré par les fonctions INC et DEC

program C_INCDEC;J J var b : byte;J I : lnteger;J w : word;.J 1 : longint:J J beglnJ lnc( b ):J cs:0008 FE063EO0 Inc byte ptr [B] ;Incrémente B 1 J inc( I );.J f cs:000C FF064000 Inc word ptr [I] ;Incrémente I de 1

lnc( W. 5 * 4 );J cs:0010 8306420014 add word ptr [W].0014 ;Ajoute une constante ).J J lnc( 1 );.i f cs:0015 8306440001 add word ptr [L].0001 ;Incrémente de 1 le )J f mot de poids faible ).J f cs:001A 8316460000 adc word ptr [L+2].0000 ;Gère la retenue Int. ).J J dec( b ):J f cs:002A FEOE3E00 dec byte ptr [B] ;Décrémente B de 1

dec( I );.J f cs:000C FF064000 dec word ptr [I] ;Décrémente I de 1

dec( w. 5 * 4 ).J f cs:0032 832E420014 sub word ptr [W].0014 ;Soustrait une constante )j

dec( 1 );.i f cs:0037 832E440001 sub word ptr [L].0001 ;Décrémente de 1 le )J f mot de poids faible ).i f cs:003C 831E460000 sbb word ptr [L+2].0000 ;Gère la retenue interne)J VI Exit Pour sortir d'une procédure/fonction ou pour terminer l'exécution du programme principal, Turbo Pascal fournit l'instruction Exit qui est transformée au niveau interne en un branchement vers la fin du programme principal ou la fin de la procédure/ fonction. Elle est donc théoriquement équivalente à un GOTO. C_EXIT: Programme de démonstration pour étudier le code machine f généré par la fonction EXIT J program C_EXIT;.i J beginJ 2.110 La Bible Turbo Pascal 2- 111 Dans les coulisses du langage exit; .J cs:001E E817 jmp end ;Aller à la fin du programme U writeln( 'Instruction inaccessible à 1—exécution .1 end label near end. V Hi, Lo D'après le manuel, les fonctions Hi et Lo permettent au programmeur de connaître l'octet de poids fort et l'octet de poids faible d'un opérande de type Word ou Integer. Le listing ci-après montre cependant que ces fonctions sont aussi capable de traiter les entiers longs. Dans ce cas, Hi et Lo traitent uniquement la partie basse de l'entier et il est impossible d'accéder à sa partie haute. Pour affecter le résultat de Hi ou Lo, Turbo Pascal est obligé de mettre en oeuvre deux instructions MOV. En effet, le 8086/8088 n'est pas capable de charger une mémoire avec le contenu d'une autre en une seule instruction. Le passage par un registre du processeur est incontournable. C_HILO: démonstration pour étudier le code machine Programme de généré par les fonctions HI et LO J program C_HILO;.J J var w word;.J 1 : longint:.J b byte;.J J begi n.J b hi( $1234 );,J cs:0008 C606440012 mov byte ptr [B].12 ;Constante! J b :- hi( w );_J cs:000D A03FOO mov al ,byte ptr [W+1] ;Charge l'octet sup, de W J. et le transfère e) C cs:0010 A24400 mov [B],al B ),i 'J b hi( 1 );.J cs:0013 A04100 mov al.byte ptr [L+1] ;Charge en AL l'octet ),i f sup.du mot sup. de L Li cs:0016 A24400 mov [B],al et le transfère en B Li 'J b lof $1234 );,J Cs:0019 C606440034 mov byte ptr [8],34 ;Constantel 'J b lof w );.J cs:000D A03FOO mov al.byte ptr [W] ;Charge l'octet inf,de W ),J cs:0010 A24400 mov [B],al le transfère en B Li J b :- lo( 1 );,J cs:0013 A04100 mov al.byte ptr [L] ;Charge l'octet inf du U [ mot inf de L Ij cs:0016 A24400 mov [B],al et le transfère en B ).J

end. V Length La fonction Length renvoie la longueur de la chaîne de caractères qu'on lui soumet. Pour donner cette information, Turbo Pascal n'a pas besoin de se livrer à de longs Dans les coulisses du langage 2-111 La Bible Turbo Pascal 2- 112 calculs comme le ferait le langage C. Elle se trouve en effet dans le premier octet de la chaîne (caractère d'indice O). L'exécution de l'instruction: n'est pas plus rapide que l'appel à Length, car la fonction ne fait pas autre chose que de renvoyer le premier octet. Si on indique comme argument une chaîne constante, elle n'est pas mémorisée parmi les constantes typées du segment de données, mais seule sa longueur est retenue. Il est par ailleurs intéressant de noter qu'à l'inverse d'autres fonctions l'affectation du résultat à une variable ne nécessite pas la mise en oeuvre d'un registre de transfert. La longueur de la chaîne est directement chargée dans la variable indiquée. Pour Turbo Pascal une expression du genre b:— Length('Test'); I est strictement équivalente à b:-4; I et dans ce dernier cas il n'est effectivement pas nécessaire d'avoir recours à un registre de transfert. C_LENGTH: Programme de démonstration pour étudier le code machine généré par la fonction LENGTH J program C_LENGTH ;..J J var I : integer;.J S : string;J J beginJ length( 'String' );J ( cs:0008 C7063E000600 mov word ptr [I],0006 ;Constantel J I :— length( s );.-I cs:000E A04000 mov al,[S] ;Prend le 1er octet de la ;chaTne et le copie dans AL ).i cs:0011 30E4 xor ah,ah ;Met à O l'octet de poids fort ).J cs:0013 A33E00 mov [I],ax et charge AX en I 1.-i end. VOdd La fonction Odd est quelque peu singulière. Elle communique au programme appelant un résultat booléen qui indique si le paramètre transmis (obligatoirement de type entier) est pair ou impair. Pour trouver ce résultat, la fonction Odd examine simplement le bit le moins significatif de l'opérande: s'il est égal à 1, en vertu des lois du système binaire, le nombre est impair. Turbo Pascal examine le contenu de ce bit en chargeant dans le registre AL l'octet de poids faible de l'opérande. Puis à l'aide de l'ordre SI-W les bits du registre sont décalés 2-112 La Bible Turbo Pascal 2- 113 Dans les coulisses du langage d'une position vers la droite. Le bit le moins significatif passe alors dans l'indicateur de retenue et sa valeur peut être connue au moyen d'un branchement JB. Si l'indicateur est à 1, la fonction Odd renvoie la valeur 1, ce qui dans le type booléen correspond à une assertion vraie (TRUE). Si l'expression transmise à Odd peut être évaluée au moment de la compilation, Turbo Pascal mémorise tout de suite le résultat TRUE ou FALSE pour ne pas être obligé de recalculer l'expression par la suite. rogramme de démonstration pour étudier le code machine - -- ).j généré par la fonction 000 Li J program CODD;.J var 1 longlnt;.J I lnteger;.J b : byte;.i bo: boolean;.J '-J begln.J bo :— odd( 1 );J t cs:0008 A03EO0 mov al.byte ptr [L] ;Charge en AL l'octet lnf).J du mot inf. t cs:000B DOE8 shr al.1 ;Décale d'un bit à droite cs:000D 7204 jb 0013 ;Retenue- 1? OUI t cs:000F B000 mov al,OO ;Non, nombre pair. Charge FALSELJ C cs:0011 EB02 jrnp 0015 C cs:0013 BOOl mov al.01 ;Oui. nbre impair. Charge TRUE U C cs:0015 A24500 L4 mov [BO],al Met le résultat en 80 Li 'J bo : odd( I );.J C cs:0018 A04200 mov al.byte ptr [I] ;Charge en AL l'octet infUi de I 1-1 j cs:001B DOE8 shr al .1 ;Décale d'un bit à droite L-J j cs:001D 7204 jb 0023 ;Retenue- 1? OUI Li cs:001F 8000 mov al .00 ;Non, nombre pair. Charge FALSE).i j cs:0021 EB02 jmp 0025 L-i cs:0023 8001 mov al 01 ;Oui. nbre Impair. Charge TRUE Li t cs:0025 A24500 mov [BO],al ;Met le résultat en 80 Li L-' bo :— odd( b );J C cs:0028 A04400 mov al [B] ;Charge en AL le contenu de B Li C cs:002B DOE8 shr al ,1 ;Comme précédemment.... Li C cs:002D 7204 jb 0033 Li C cs:002F B000 mov al 00 C cs:0031 EB02 jmp 0035 Li C cs:0033 BOOl mov al .01 Li t cs:0035 A24500 L_9 mov [BO],al L-i .i bo :— odd( 17 + 4 );J cs:0038 C606450001 mov byte ptr [BO].O1 ;Constante! '-I end. V Seg, Ofs Les fonctions Seg et Ofs servent à donner l'adresse du segment et l'offset d'une variable, d'une constante typée ou d'une procédure/ fonction. L'une et l'autre fonction ont beaucoup de points communs avec la fonction Addr qui retourne également une adresse, mais sous forme de pointeur. Dans les coulisses du langage 2-113 La Bible Turbo Pascal 2-114 Ici aussi, lorsqu'on accède à des variables absolues ou des variables référencées par un pointeur, le segment et le déplacement sont mémorisés dans ES:SI ou ES:DI. Cette particularité n'est pas clairement explicable. La fonction Seg montre bien la différence entre variable locale et variable globale: dans le premier cas, c'est la valeur de SS qui est renvoyée, dans le second cas celle de DS. La différence est aussi mise en évidence par Ofs. Le déplacement d'une variable globale est une constante qui peut être chargée dans l'un des registres, tandis qu'une variable locale est toujours repérée par un adressage relatif à BI', dont la valeur n'est pas encore connue au moment de la compilation. Le langage doit alors utiliser l'instruction LEA pour calculer le déplacement de la variable par rapport au contenu de BI'. C_SES0FS: Programme de démonstration pour étudier le machine J généré par les fonctions 0ES et SES J J program C_SEGOFS;.J .J type tamp — array[ l..100 J of BYTE-..j bp Atamp;J var w : word;.J varglob : integer;J bios_flag : byte absolute $0040:$0049;.J J procedure test:.J -J var varloc : integer;.J 1 begin.J w :— Seg( varglob );.J C cs:000E 8CD8 C cs:0010 A33E00 .J W : Seg( varloc );J C cs:0013 8C00 C cs:0015 A33E00 -j w :— Seg( bios_flag );J C cs:0018 BE4000 C cs:0018 8EC6 C cs:0010 8CCO C cs:001F A33E00 (-j il aurait suffi de: -j w :— Ofs( varglob ):J C cs:0022 884000 C cs:0025 A33E00 -j w :— Ofs( varloc );J C cs:0028 8D46EE E cs:002B A33E00 J w :— Ofs( biosflag );.J E cs:002E BE4000 ( cs:0031 8EC6 C cs:0033 B84900 C cs:0036 A33E00 (-j il aurait suffi de: J end ;J J begin.J test;J end. mov ax.ds ;Copie OS en AX mov [W].ax et de là en W mov ax.ss ;Copie SS en AX mov [W].ax ; et de là en W mov si .0040 ;Segment de BIOS_FLAG )J mov essi copié en ES C???) )J mov ax.es ; puis en AX C???) )J mov [W].ax ; et pour finir en W ... mov word ptr [W] .0040 mov ax.offset VARGLOB;Met le déplacement en AX )J mov [W].ax et en W lea ax.[bp-02] ;Adresse relative à BP mov [WJ.ax mémorisée en W mov si .0040 ;Segment (I) de BIOS_FLAS mov es.si ; transféré en ES mov ax.0049 ;Déplacement mis en AX mov [W].ax ; puis en W mov word ptr [W].0049 2-114 La Bible Turbo Pascal 2-115 Dans les coulisses du langage Ord La fonction Ord renvoie le rang (ou "position ordinale') d'un argument de type scalaire. Sa mise en service ne se justifie pratiquement qu'avec le type énuméré et pour connaître le code ASCII d'un opérande caractère, car le rang d'un type entier est égal à sa valeur numérique. Pour connaître le rang d'une variable de type énuméré, Turbo Pascal se contente de renvoyer le code interne qui sert à gérer ce type. La tâche est également très simple lorsqu'il s'agit de trouver le code d'un caractère ASCII. Il suffit au compilateur de renvoyer le contenu du caractère. Le listing suivant permet de vérifier ces traitements: C_ORD: Programme de démonstration pour étudier le code machine généré par la fonction ORD 'J program C_ORD;.J J type couleurs - (ROUGE. JAUNE. BLEU);J J var couleur : couleurs;J I integer;.J 1 : longint;J J begln.J Ord( couleur );.J f cs:0008 A03EO0 mov al,[COULEUR] ;Prend le contenu de couleur ).-i f cs:000B 98 cbw le convertit en un entier )j f cs:000C A34000 mov CI].ax ; et le stocke en I .J :— Ord( ROUGE );.J ( cs:000F 31C0 xor ax,ax ;Le code interne de ROUGE —O ).-J f cs:0011 A34000 mov [I],ax ;Il est stocké en I J :— Ord( JAUNE );.J f cs:0014 C70640000100 mov word ptr [I].0001 ;Constantel 1 :— Ord( BLEU );.J f cs:001A C70640000200 mov word ptr [I].0002 ;Constante!

I :— Ord( 'A' );.J f cs:0020 C70640004100 mov word ptr [IJ.0041 ;Code ASCII de A .J I :— Ord( 1 );.J ( cs:0026 A14200 mov ax,[L] ;Charge le mot inférieur de L ).J cs:0029 A34000 mov EI).ax :et le prend pour résultat "Pi Du point de vue du compilateur, Pi n'est rien d'autre qu'une constante prédéfinie à l'intérieur de l'unité System. Chaque fois que PI apparaît dans un listing source, Turbo Pascal le remplace par sa valeur. Le programme suivant montre clairement ce processus. Ici PI est affecté à une variable réelle. Turbo Pascal charge donc PI dans la variable indiquée sous la forme d'une constante réelle formée de six octets. C'est simple et efficace, il n'y a rien à optimiser. Dans les coulisses du langage 2-115 La Bible Turbo Pascal 2-116 C_PI: Programme de démonstration pour étudier le code machine permettant d'accéder à la constante PI -J program C_PI;.J J var r : real ;.J begi n.J r :- PI;.-' f cs:0008 C7063E008221 mov word ptr [R] .2182 ;Charge Pi sous forme)J f cs:000E C7064000A2DA mov word ptr [R+2].DAA2 ; d'une constante )J f cs:0014 C70642000F49 mov word ptr [R+4],490F ; réelle de 6 octets ).J end. V Pred,Succ Pred et Succ renvoient le prédécesseur ou le successeur de leur argument, qui doit être de type scalaire. Lorsqu'on soumet un type entier à cette fonction, l'opération est équivalente à une incrémentation ou une décrémentation. Sous la version 3.0, cette utilisation était intéressante car elle est plus rapide qu'une affectation du genre: x:-x+1; I A partir de la version 4.0, Turbo Pascal propose les procédures Inc et Dec qui sont encore plus rapides que Pred et Succ. Elles ne génèrent pas trois mais un seul ordre machine lorsqu'elles travaillent sur des entiers de type Byte ou Word. Avec les entiers longs de type Longlnt, l'amélioration est dans le même rapport: Pred et Succ nécessitent six instructions machine, alors que Inc et Dec n'en prennent que deux. En général Pred et Succ ne sont mis en service que pour les types énumérés. A la compilation, le compilateur cherche à reconnaître si l'argument est une constante ou une expression qui ne peut être évaluée qu'au moment de l'exécution du programme. Dans le premier cas, Turbo Pascal mémorise immédiatement la valeur du prédécesseur ou du successeur pour éviter de la recalculer à l'exécution. En même temps, Turbo Pascal contrôle que l'intervalle de définition est bien respecté, comme le montre la dernière instruction du programme ci-dessous: C_PRESUC: Programme de démonstration pour étudier le code machine généré par les fonctions PREC et SUCC 'J program C_PRESUC;J 'J. type couleurs - (ROUGE. JAUNE. BLEU );.J 'J var couleur : couleurs;J 1 : longlnt;J b : boolean;J 'J begin.J couleur :— Pred( BLEU );.J f cs:0008 C6063E0001 mov byte ptr [COULEUR].01 ;Constantel J couleur :- Pred( couleur );.J cs:0000 A03EO0 mov al.[COULEUR] ;Charge la couleur actuelle 1.-i f cs:0010 FEC8 dec al ; la décrémente f cs:0012 A23E00 mov [COULEUR].al ; et la remet dans la var. ).J -J b :— Pred( b );_J f cs:0015 A04400 mov al.[B] ;Prend le contenu de B f cs:0018EC_ dec al ; le décrémente 2-116 La Bible Turbo Pascal 2- 117 Dans les coulisses du langage C cs:001A A24400 mov 'J 1 :- Pred( 1 );J C cs:OO1D C4064000 les

C cs:0021 8CC2 mov C cs:0023 2D0100 sub C cs:0026 830A00 sbb C cs:0029 A34000 mov C cs:002C 89164200 mov couleur :- Succ( couleur );.J ( cs:0030 A03EO0 mov C cs:0033 FECO Inc C cs:0035 A23E00 mov [B].al et le réexpédie dans B ).J ax.[L] ;Copie le contenu de L Li en ES:AX dx.es et de là en DS:AX Li ax.0001 ;Décrémente le mot inf. Li dx,0000 ;Tient compte du débordement ).J [L],ax ;Mémorise le résultat [L+2].dx al.[COULEUR] ;Charge la couleur actuelle ).J al l'incrémente Li [COULEUR].al ; et la remet dans la var. Li -J b :- Succ( b );_J C cs:0038 A04400 mov al.[B] ;Prend le contenu de B Li C cs:003B FECO inc al l'incrémente cs:003D A24400 mov [B].al ; et le remet en place Li .J 1 :- Succ( 1 ):J cs:0040 C4064000 les ax,[L] ;Copie le contenu de L Li C en ES:AX ) 'J C cs:0044 8CC2 mov dx.es de là en DX:AX Li C cs:0046 050100 add ax,0001 ;Incrémente le mot inf. Li C cs:0049 830200 adc dx,0000 ;Tient compte du débordement Li C cs:004C A34000 mov [L],ax ;Mémorise le résultat Li C cs:004F 89164200 mov [L+2],dx -J C$ifdef ERREUR Li couleur :- Succ( BLEU ); C Error: Constant out 0f Range Li ($endi f).J J end. 'Ptr La fonction Ptr met à rude épreuve l'intelligence du compilateur Turbo Pascal. Rappelons qu'elle est destinée à renvoyer un pointeur non typé à partir d'un ensemble de deux adresses définissant un segment et un déplacement. Turbo Pascal doit d'abord faire la distinction entre constantes et expressions variables. De plus, les constantes sont examinées pour que le cas du pointeur MIL (segment et déplacement nuls) soit traité à part. Dans le cas général, les segments et déplacements constants sont chargés dans les parties correspondantes d'un pointeur. Dans le cas du pointeur NIL, les choses se passent autrement. Le registre AX est mis à O par un XOR et il est ensuite chargé dans les deux composants du pointeur. Par rapport à la méthode générale, il faut une instruction supplémentaire mais la séquence est plus rapide car les ordres MOV opérant sur les registres sont plus rapides que ceux qui opèrent sur des constantes. CPTR: Programme de démonstration pour étudier le code machine C généré par la fonction PTR J program C_PTR;.J 'J var p pointer;J J begi nJ p :- ptr( 0. 0 );J C cs:0008 31C0 xor ax.ax ;Met AX â O ( cs:000A A33E00 mov [P],ax copie cette valeur dans )J C cs:0000 A34000 mov [P+2].ax les deux parties de P (segment )J Dans les coulisses du langage 2-117 La Bible Turbo Pascal 2-118 C et déplacement ) p ptr( 100. 100 );.J f cs:0010 C7063E006400 mov word ptr [P].0064 ;Constantesl f cs:0016 C70640006400 mov word ptr [0040],0064 -J p ptr( $8000. $0000 );J f cs:001C C7063E000000 mov word ptr [P].0000 :Idem[ f cs:0022 C706400000B0 mov word ptr [0040].B000 end. V Sizeof La fonction SizeOf renvoie le nombre d'octets de mémoire occupés par l'argument, qui peut être une variable ou un type de variable. Cette information étant connue dès la compilation, SizeOf devient une constante traitée comme telle par le compilateur. C_SIZEOF: Programme de démonstration pour étudier le code machine généré par la fonction SIZEOF J program C_SIZEOF;.J -J type tampon - array [l..1000] of byte;J J var i : lnteger;.J b : byte;.J J beginJ

- sizeof( tampon );J

f cs:0008 C7063E00E803 mov b :- sizeof( j );.J f cs:000E C606400002 mov J ($ifdef ERREUR)J b :- sizeof( tampon ); ($endi f).J J end. V Write et Writeln word ptr [I].03E8 ;Constantel byte ptr [.B].02 ;idem Error: Constant out 0f Range ).i ).J Avec Read et Readln, les procédures Write et Writeln sont les seules procédures de Pascal qui fonctionnent avec un nombre variable d'arguments. Le code généré par ces procédures présente donc des caractères particuliers. S'il n' y a guère de différence entre les appels à Write et Writeln, Turbo Pascal effectue une stricte distinction entre les sorties sur fichiers typés et les sorties sur fichiers texte. Fichiers texte Du point de vue du programmeur, les procédures Write et Writeln ne concernent que rarement des fichiers. Le plus souvent, elles sont employées pour afficher des informations sur l'écran. Mais l'écran, selon le point de vue de Turbo Pascal, ne constitue qu'un fichier texte particulier. Si Write/Writeln sont utilisés sans qu'une variable fichier soit transmise comme premier paramètre, Turbo Pascal met automatiquement en service la variable fichier Output qui est définie dans l'unité System et qui est liée à l'écran. 2-118 La Bible Turbo Pascal 2-119 Dans les coulisses du langage La première action effectuée par le compilateur consiste à mettre sur la pile un pointeur qui pointe sur cette variable. Les routines qui suivent peuvent alors reconnaître l'adresse de la sortie et se mettre en relation avec le fichier ou le périphérique correspondants (imprimante ou écran). Après s'être occupé de la variable fichier, Turbo Pascal traite tous les arguments à émettre en partant de la gauche et en progressant vers la droite. Chaque argument provoque un appel de sous-programme. A l'inverse de ce qui se passe pour une procédure/ fonction usuelle, les différents arguments ne sont pas empilés globalement avant d'être exploités dans leur totalité par le sous-programme, mais ils sont traités un à un par des routines spécialisées. Pour chaque type il existe une routine adaptée qui est offerte par l'unité System. Les arguments de type String, Cgar, Boolean, et Real sont traités séparément, mais il n'existe qu'une seule routine pour tous les types entiers. Pour être aussi générale, cette dernière attend son argument sous forme d'entier long. Les arguments de type plus petit doivent donc être au préalable convertis au format Longlnt sur 4 octets. Après avoir empilé cet argument, Turbo Pascal dépose encore sur la pile la longueur du champ (d'affichage, d'impression ou d'enregistrement). Cette indication peut figurer à l'intérieur des parenthèses de Write juste après l'argument à émettre, separée de ce dernier par deux-points. Si elle n'est pas donnée, Turbo Pascal lui affecte la valeur O, ce qui provoque l'émission du nombre de caractères juste suffisants pour représenter l'argument à émettre. Les routines de sortie pour les types String, Char, Boolean et Real attendent aussi comme argument un mot exprimant la longueur du champ. Turbo Pascal écrit également O si cette donnée fait défaut. La routine de sortie des réels attend encore un argument supplémentaire décrivant le nombre de décimales à émettre. Là encore, faute de cette indication, c'est la valeur O qui est prise. Tous ces arguments sont transmis sous les formes les plus diverses aux routines qui doivent les assimiler. Les caractères et les booléens sont empilés au format Word. Les chaînes de caractères sont transmises sous forme de pointeur. Les nombres en virgule flottante sont déposés sur la pile comme une suite de 6 octets, ce qui est la mémorisation habituelle des réels. Toutes les mutines prennent soin de retirer les arguments de la pile avant de rendre la main au programme appelant: cette manière de procéder est typique de Pascal. Seul le pointeur sur la variable fichier est laissé intact, car d'une routine interne à l'autre, il va être réutilisé. Ce n'est qu'à l'issue de la dernière routine interne, donc après le traitement du dernier argument de Write/Writeln, que le pointeur sur la variable fichier est retiré de la pile. Cette tâche est prise en charge par la routine de clôture de la procédure Write/Writeln. Il faut attendre cette mutine de clôture pour voir apparaître sur l'écran les arguments demandés. Jusqu'ici, ceux-ci étaient simplement convertis en chaînes ASCII mises bout à bout dans une mémoire tampon. La routine de clôture vide la mémoire tampon et dépile le pointeur sur la variable fichier. Selon qu'on a invoqué Write ou Writeln, la mutine de clôture ajoute ou non la combinaison des caractères <CR/LF> (Retour chariot et passage à la ligne suivante). Dans les coulisses du langage 2-119 La Bible Turbo Pascal - 110 Le listing suivant montre quelques exemples de traduction d'instructions Write et Wnteln. program writest;.J 'j uses Printer;J 'j var b : byte;.J i : integer;.J bo : boolean;J C : char;.J r : real;.J 'j begi n_1 writeln( 1:3, b );.J f cs:0012 BF4AO2 mov di,offset OUTPUT ;Déplacement de Output ).J cs:0015 lE push ds ;Empile un pointeur FAR U. f cs:0016 57 push di sur Output ).J [ cs:0017 A14000 mov ax,[I] Charge I en AX cs:OO1A 99 cwd le convertit en Longlnt cs:OO1B 52 push dx et le met sur la pile ).J f cs:001G 50 push ax cs:OO1D B80300 mov ax,0003 ;Charge la longueur du champ cs:0020 50 push ax et la met sur la pile cs:0021 9A440E9F5F call FlntÀrg :Formate un argument entier Li f cs:0026 A03EO0 mov al,[B] ;Charge B en AL Li cs:0029 30E4 xor ah.ah le convertit en Word Li cs:002B 3102 xor dx,dx ;Le mot sup. du Longlnt est O )J f cs:002D 52 push dx ;Transfère le Longlnt de DX:AX Li cs:002E 50 push ax sur la pile 3_1 cs:002F 31GO xor ax,ax ;Pas d'indication de longueur Li cs:0031 50 push ax met O sur la pile cs:0032 9A440E9F5F call FlntArg ;Formate un argument entier Li f cs:0037 9AB4OC9F5F call WlnFlush ;Appelle WritelnFlush f affiche le contenu du tampon).J f Efface le tampon ).J Retire de la pile le f pointeur sur Output 'j writeln( 'Test', bo, c f cs:003C BF4AO2 mov di,offset OUTPUT ;Déplacement de Output ).j cs:003F lE push ds ;Empile un pointeur FAR f cs:0040 57 push di sur Output cs:0041 BF0000 mov di,0000 ;Empile comme pointeur FAR 3_i cs:0044 0E push cs l'adresse de la chaîne à 3_1 f cs:0045 57 push di ;ficher 3_i cs:0046 31GO xor ax,ax ;Pas d'indication de longueur ).J C cs:0048 50 push ax ( cs:0049 9A7C009F5F call FStrArg ;Formate un argument chaîne ).j C cs:004E A04200 mov al.[B0] ;Charge BO en AL cs:0051 50 push ax et le dispose sur la pile 3_i ( cs:0052 31GO xor ax.ax ;Pas d'indication de longueur ).i f cs:0054 50 push ax f cs:0055 9ABAOD9F5F call FBoolArg ;Formate un argument booléen ).J f cs:005A A04300 mov al.[CJ ;Charge G en AL f cs:005D 50 push ax et le met sur la pile f cs:005E 31GO xor ax.ax ;Pas d'indication de longueur ).J f cs:0060 50 push ax 3_i f cs:0061 9A17OD9F5F call FCharÀrg ;Formate un argument caractère ).J f cs:0066 9AB40G9F5F call WlnFlush ;WritelnFlush, cf. supra I,J J writeln( lst, r:8:4 );J f cs:0068 BF4AO0 mov di,offset LST ;Déplacement de LST 3_J j cs:006E lE push ds ;Empile un pointeur FAR 3_1 f cs:006F 57 push di ;r LST f cs:0070 FF364800 push £R+41 ;Met aussi sur la pile les 3_i f cs:0074 FF364600 push [R+2] 6 octets de la variable réelle).J f cs:0078 FF364400 push [RI 3.1 2-120 La Bible Turbo Pascal 2-121 Dans les coulisses du langage - cs:007C B80800 mov ax.0008 ;Longueur du champ de sortie )J cs:007F 50 push ax à mettre sur la plie cs:0080 B80400 mov ax.0004 ;Nombre de décimales cs:0083 50 push ax ;mettre sur la plie cs:0084 9AE70E9F5F call FRealArg ;Formate un argument réel cs:0089 9AB40C9F5F call WinFlush ;WrlteinFiush, cf. supra ).J

wrlte( Test );J ( cs:008E BF4AO2 mov di,offset OUTPUT ;Toujours la même chose:)_i cs:0091 lE push ds ;Empile un pointeur FAR qui L-i cs:0092 57 push di ; pointe sur Output ).J cs:0093 BF0000 mov di.0000 ;Empile un pointeur qui pointe ).J cs:0096 0E push cs ; sur la chaîne de caractères Li cs:0097 57 push di L-i cs:0098 31C0 xor ax,ax :Pas de longueur du champ L-i cs:009A 50 push ax L-i cs:009B 9A7C009F5F call FStrArg ;Formate un argument chaîne L-i cs:OOAO 9AD30C9FSF call WFlush ;cette fois ce n'est pas U WritelnFlush. mais WrlteFlush) end. Fichiers typés La compilation de Write en relation avec des fichiers typés ressemble au schéma précédent utilisé pour les fichiers texte. Rappelons à tout hasard que WnteLn n'est autorisé qu'avec des fichiers texte. Turbo Pascal commence par mettre sur la pile un pointeur qui référence la variable fichier dont le nom est à communiquer comme premier argument de la procédure Write. Les autres arguments sont pris en compte de gauche à droite. Si une variable est désignée nommément, son adresse est empilée. Si elle est référencée par un pointeur, c'est le contenu du pointeur qui est mis sur la pile. Puis, indépendamment du type de la variable, une routine interne de l'unité System est appelée pour écrire l'argument transmis dans le fichier désigné par la variable fichier. La routine tire de la variable fichier la longueur des variables et par conséquent le nombre d'octets à émettre. Avant de rendre la main au programme appelant, la routine retire de la pile l'argument transmis, mais elle prend soin d'y laisser le pointeur qui référence la variable fichier pour qu'il puisse servir à nouveau. Les arguments sont ainsi traités un à un, les adresses des variables étant disposées sur la pile et la routine interne de sortie étant à chaque fois invoquée. Ici il n'ya pas comme précédemment de mémorisation transitoire dans une mémoire tampon: la mutine de sortie écrit directement chaque argument dans le fichier de sortie. Il n'est donc pas nécessaire de faire appel à une routine de clôture. Le programme se termine simplement en retirant de la pile le pointeur sur la variable fichier. Pour cela, il ajoute la valeur 4 au pointeur de pile. program writestl ;.J J type Info - record.J Nom string[ 30 ];J Prenom : string[ 20 ];.J Ilatric integer;.J end;J J var Linelnfo : Info;J 1.j lnteger;J Dans les coulisses du langage 2-121 La Bible Turbo Pascal 2- 122 fe : file of Info;.J fi : file 0f Integer:.J J begi n.J write( fi. 1. j );.J C cs:0008 BFF800 mov di.offset FI ;Déplacement de FI C cs:000B lE push ds ;Met sur la pile un pointeur Li cs:000C 57 push di qui pointe sur FI C cs:000D BF7400 mov di,offset I ;Déplacement de I cs:0010 lE push ds ;Met sur la pile un pointeur Li cz:0011 57 push di qui pointe sur I C cs:0012 9A7005945F call Write2File ;Enregistre l'argument dans Li le fichier f cs:0017 BF7600 mov di .offset J ;Déplacement de J C cs:001A lE push ds ;Met sur la pile un pointeur Li cs:001B 57 push di qui pointe sur J f cs:001C 9A7005945F call Write2File ;Enregistre l'argument dans Li C ; le fichier Li C cs:0021 83C404 add sp.0004 ;Retire de la pile le pointeur FAR qui pointait ).i sur la variable fichier ).J -j write( fe, Unelnfo );.J C cs:0024 BFF800 mov di,offset FE ;Déplacement de FE Li C cs:0027 lE push ds ;Met sur la pile un pointeur C cs:0028 57 push di qui pointe sur FE II_i f cs:0029 BF7400 mov di,offset Unelnfo ;Déplacement de Uneinfo ).J C cs:002C lE push ds ;Met sur la pile un pointeur ).i cs:002D 57 push di qui pointe sur Unelnfo ).i cs:002E 9A7005945F call Write2File ;Enregistre l'argument dans Li le fichier ).j f cs:0032 83C404 add sp,0004 ;Retire de la pile le ).J pointeur FAR qui pointait ).i f sur FE.J end. V Read et Readin Les procédures standard Read et Readin sont étroitement apparentées aux procédures Write et Writeln, mais au lieu d'écrire des caractères dans un fichier ou un périphérique, elles lisent des caractères issus d'un fichier ou d'un périphérique. Leur syntaxe est identique à celle de Write/Writeln, ainsi que leur compilation, sauf que les routines internes appelées sont des routines d'entrée et non de sortie. Lorsque par Read/Readln on accède à des fichiers texte sans indiquer de variable fichier, une variable fichier standard appelée INPUT et liée au clavier entre en jeu. Tout comme OUTPUT, la variable fichier INPUT est définie et gérée par l'unité System. V New, Dispose et consorts Les procédures New et Dispose, qui permettent d'allouer de la mémoire sur le tas puis de la libérer, sont également définies par l'unité System. Alors que New alloue juste la mémoire correspondant au type du pointeur transmis, GetMem offre plus de liberté en allouant librement toute la mémoire que l'on veut. Les deux procédures New et GetMem ont chacune une contrepartie destinée à libérer la mémoire: Dispose et FreeMem. 2-122 La Bible Turbo Pascal 2- 123 Dans les coulisses du langage La question qui se pose est de savoir ce qui distingue les couples (New, Dispose) et (GetMem, FreeMem) . Un coup d'oeil sur le code machine généré apporte des révélations étonnantes: New et Dispose ne sont que des coquilles vides qui dissimulent des appels à GetMem et FreeMem. GetMem attend comme argument la taille du bloc à allouer et retourne un pointeur sur ce bloc s'il reste assez de place sur le tas. Le pointeur que le programme appelant doit passer à GetMem n'est pas réellement transmis au sous-programme interne, mais il est chargé avec le résultat de l'allocation. Autrement dit, au niveau interne, GetMem se comporte comme une fonction et non comme une procédure. Dans l'appel à New il manque toujours l'indication de la taille souhaitée. Mais le pointeur donné doit être obligatoirement typé. Turbo Pascal connaît de ce fait la taille de la mémoire à allouer et peut la transmettre à GetMem. La procédure FreeMem est le pendant de GetMem et désalloue la mémoire précédemment réservée. Elle a donc besoin de l'adresse du bloc mémoire sous la forme du pointeur renvoyé par GetMem et de sa taille, qui constituent les deux arguments. Dans le cas de Dispose, la taille n'est pas indiquée, mais elle est déduite du type du pointeur utilisé. program newtest.J -J type tampon - array [l..1000] 0f Word;J .J var tamptr : tampon;.] p pointer;-] 'J begin.J New( tamptr );J cs:0008 B80007 mov ax.07D0 ;Le nbre d'octets occupés par ).J cs:000B 50 push ax ; un TAMPON est mis sur la pile)'J ( cs:000C 9A0702965F call GetMem ;Appelle GetMem { cs:0011 A33EO0 mov [TAMPTR],ax ;Stocke en TAMPTR le cs:0014 89164000 mov [TAMPTR+2],dx pointeur FAR retourné GetMem( p. 2000 );J instruction différente, mais code identique ),J cs:0018 880007 mov ax,0700 ;Alloue 2000 octets cs:001B 50 push ax en appelant ( cs:001C 9A0702965F call GetMem GetMem cs:0021 A34200 mov [P],ax ;Stocke en P le pointeur cs:0024 89164400 mov [P+2].dx FAR renvoyé 'J Dispose( tamptr );.J cs:0028 FF364000 push [TAMPTR] ;Empile l'adresse du bloc ),J cs:002C FF363EO0 push [TAMPTR+2] alloué à TAMPTR cs:0030 880007 mov ax,07D0 ;Le nombre d'octets alloués ).i cs:0033 50 push ax est mis sur la pile [ cs:0034 9A1F02965F cali FreeMem ;Appelle FreeMem

FreeMem( p. 2000 );,J Encore une autre instruction, et le même code I cs:0039 FF364400 push [P] ;Empile l'adresse du bloc cs:003D FF364200 push [P+2] alloué à P cs:0041 B80007 mov ax,07D0 ;Le nombre d'octets alloués ).J cs:0044 50 push ax ;t aussi mis sur la pile ( cs:0045 9A1F02965F cali FreeMem ;Appelle FreeMern end. Le mode de traduction des appels à New et Dispose empêche malheureusement le détournement des procédures GetMem et FreeMem, ce qui permettrait par exemple Dans les coulisses du langage 2-123 La Bible Turbo Pascal 2-124 d'exploiter par ce moyen la mémoire paginée ou étendue. Si on redéfinit les procédures GetMem et FreeMem sous le même nom, les nouvelles versions sont effectivement invoquées lorsqu'on les appelle directement dans la source mais au niveau du code généré pour New et Dispose, ce sont les anciennes mutines qui sont reprises. 2.8. Aide au développement Contrôle des domaines de validité (Range checking) ..................124 Contrôle de la pile (Stack checking) .................................130 Contrôle des entrées-sorties (I/O checking) ..........................132 Erreurs de programmation, bugs, défauts: nous connaissons tous ces plaies. Pendant le développement d'un programme, ils nous collent aux talons et si par hasard tout marche du premier coup, le phénomène attire la méfiance et la suspicion. Une variable qui n'a pas la valeur attendue, un écran qui n'a pas l'aspect souhaité, une imprimante qui imprime n'importe quoi, tout cela, ce ne sont que des problèmes mineurs. Le véritable malaise apparaît lorsque l'ordinateur se met en rade, que l'image familière de l'environnement de développement intégré disparaît de l'écran et que malgré le martèlement effréné des touches Ctrl-Break elle refuse de réapparaître. Si vous n'avez pas sauvegardé votre source avant de lancer l'exécution, vous êtes bon pour recommencer une partie de votre travail. Pour vous permettre de garder le contrôle de la situation, Turbo Pascal vous donne le moyen d'intercepter les erreurs d'exécution (Run Time Error). Au lieu de subir un crash, vous pourrez atterrir en douceur. Nous allons examiner successivement le contrôle des domaines de validité (Range Checking), le contrôle des entrées-sorties (I/O Checking) et le contrôle de la pile (Stack Checking). V Contrôle des domaines de validité (Range Checking) Ce contrôle est très utile. Lorsqu'il est en place et que le domaine de validité d'une expression se trouve dépassé, le programme est interrompu par l'erreur d'exécution n 201 (Range Check Error). En pratique le contrôle est effectué dans les circonstances suivantes: calcul d'une expression scalaire à affecter à une variable • calcul d'une expression scalaire à transmettre comme paramètre d'une fonction ou d'une procédure indexation des tableau et des chaînes de caractères Les pointeurs ne sont pas systématiquement vérifiés, car Turbo Pascal interdit les calculs arithmétiques susceptibles de leur occasionner des débordements. En principe, un pointeur Turbo Pascal peut contenir n'importe quelle adresse. 2-124 La Bible Turbo Pascal 2-125 Dans les coulisses du langage Les opérations en virgule flottante ont un contrôle intégré effectué par les routines arithmétiques et éventuellement à la charge du coprocesseur s'il est présent. Le contrôle des domaines de validité pour les types scalaires peut être globalement enclenché ou débranché à l'aide de l'option "Compiler/Range Checking" du menu "Options". Pour le mettre en service sélectivement dans telle ou telle portion de programme, il suffit d'utiliser les directives de compilation ($R+) / ($R-). Par défaut Turbo Pascal n'effectue pas ce type de contrôle. Le contrôle des domaines de validité permet de trouver des erreurs que le debugger n'aurait guère pu déceler. Beaucoup d'erreurs inexpliquées sont dues à un accès erroné aux chaînes de caractères et aux tableaux. Si un indice trop grand est utilisé, le programme écrit dans des blocs de mémoire qui n'appartiennent plus aux variables visées. Le programmeur se trouve alors confronté à un phénomène étrange : une variable change spontanément de valeur alors qu'aucune instruction ne la mentionne. Bien que le contrôle des domaines de validité constitue un moyen pratique de prévention des erreurs. Note : Lorsque le contrôle de validité est activé le programme est moins performant. Il faut donc laisser l'option active pendant la période de mise au point et la désactiver par la suite. Le code machine généré montre immédiatement pourquoi le fichier exécutable se trouve gonflé et pourquoi sa vitesse est ralentie. Avant chaque affectation de variable, avant chaque transmission de paramètre à une procédure ou une fonction, avant chaque accès à un tableau ou une chaîne de caractères, Turbo Pascal appelle une routine de vérification. Les concepteurs de Turbo Pascal ont visiblement destiné ce contrôle à la phase de développement. En effet, une seule et même routine en est responsable pour tous les types scalaires. Or ces types occupent entre I octet (Byte, Shortlnt, Char et Booelan) et 4 octets (Longlnt). Comme la routine de contrôle ne connaît pas le type de l'opérande, elle travaille fondamentalement sur les 4 octets d'un entier long. Les types plus petits sont d'abord étendus à 4 octets, ce qui nécessite déjà une ou deux instructions machine supplémentaires. La routine de contrôle attend la valeur à examiner dans la paire de registres DX:AX, DX recevant le mot de poids fort et AX le mot de poids faible. Comme le domaine de validité dépend des différents types examinés, il faut fournir une information appropriée à la routine. Cette information est déposée dans le registre DI, qui normalement ne sert pas à faire des calculs. Le programme ci-dessous montre le code généré lorsque le contrôle des domaines de validité est activé. Il affecte des valeurs à tous les types scalaires existants ainsi qu'au type énuméré Marque et à deux types chaînes de caractères. Notez bien les instructions machine CBW et CWD qui servent à donner aux opérandes une longueur standard de 4 octets. CBW signifie "Convert Byte to Word" et sert à reporter dans AH le signe de AL de façon que le registre AX reçoive le contenu étendu de AL. CWD ("Convert Word to Double") fonctionne de façon analogue: le signe de A)( est mis dans Dans les coulisses du langage 2-125 La Bible Turbo Pascal DX de façon que le couple DX:AX soit un opérande de type DWORD avec le contenu étendu de AX. program RCHECK;.J J type Marque - C VU, Audi, BMW. Benz, Nissan, Opel.,.J Ford, Volvo, Renault, Peugeot, Flat ):J 'J var Voiture : Marque;.i 1 : longlnt;,J I lnteger;.J w : word;.J b : byte:J si shortint;.J bo : boolean;J S string;.i s8 : string[8];.1 '-J procedure test;,i 'J begin.i b b - 200;.J ( cs:0032 A04800 mov al,[B] ;Copie B en AL cs:0035 30E4 xor ah.ah et convertit en mot ( cs:0037 2DC800 sub ax.00C8 ;Retranche 200 cs:003A 99 cwd ;Convertit le résultat en Li entier long Longlnt cs:003B BF0000 mov di,0000 ;Charge en DI l'adresse des li -Li mites du domaine de validité ).i cs:003E 9A1CO2E15F cali RCheck ;Contrôle la validité cs:0043 A24800 mov [B],al ;Résultat correct mémorisé ),J

si :- si * 4.J C cs:0046 A04900 mov al,[SI] Copie SI en AL Li cs:0049 98 cbw et le convertit en un entier ).i cs:004A 890200 mov cx.0002 ;Nbre de positions à décaler Li f cs:004D 03E0 shl ax.cl ;AX AX * 4 ).J cs:004F 99 cwd ;Convertit le résultat en un entier long Longlnt cs:0050 BF0800 mov dl,0008 ;Charge en DI l'adresse des li-Li mites du domaine de validité )j cs:0053 9A1CO2E15F call RCheck ;Contrôle la validité f cs:0058 A24900 mov [SI],al,J -J ax.[L] ;Copie L en ES:AX Li dx,es et de là en DX:AX ax.86A0 ;Addition mot de poids faible ),J dx,0001 ;Addition mot de poids fort ).J di,0005 ;Charge en DI l'adresse des li-Li mites du domaine de validité )j RCheck ;Contrôle la validité [L],ax ;Résultat correct mémorisé Li [L+2].dx Li al,[B0] ;Copie BO en AL Li al ;Décrémente (pred) Li ;Convertit en Integer Li et en Longlnt dl,0010 ;Charge en DI l'adresse des li-Li mites du domaine de validité Li RCheck ;Contrôle la validité [B0].al ;Si résultat 0K, le stocke Li al,[Voiture] Charge Voiture en AL al ;Incrémente (succ) Li ;Convertit en Integer Li 1 + 100000;.] cs:0058 C4064000 les cs:005F 8CC2 mov cs:0061 05A086 add cs:0064 830201 adc cs:0067 BF0500 mov cs:006A 9A1CO2E15F cal 1 cs:006F A34000 mov cs:0072 89164200 mov bo :- pred( bo );,J f cs:0076 A04AO0 mov f cs:0079 FEC8 dec f cs:007B 98 cbw f cs:007C 99 cwd C cs:007D BF1000 mov cs:0080 9A1CO2E15F cal 1 f cs:0085 A24A00 mov J Voiture :- Succ( Voiture );.J f cs:0088 A03EO0 mov f cs:008B FECO inc ( cs:008D 98 cbw 2-126 La Bible Turbo Pascal 2- 127 Dans les coulisses du langage cs:008E 99 cwd et ensuite en Longlnt Li cs:008F BF1800 mov dl.0018 ;Charge en DI l'adresse des li-Li mites du domaine de validité ).j j cs:0092 9A1CO2E15F cal] RCheck :Contrôle la validité cs:0097 A23E00 mov [Voiture],al ;Valeur correcte Li J s[ I I : 'A' ;.J ( cs:009A A14400 mov ax,[I] :Copie I en AX j cs:0090 99 cwd ;Convertit en Longlnt cs:009E BF0000 mov di,0000 ;Charge en DI l'adresse des li-).J mites du domaine de validité ).j cs:OOA1 9A1CO2E15F call RCheck ;Contrôle la validité t cs:00A6 8BF8 mov dl,ax :Si Index 0K, le met en DI ),j cs:00A8 C6854C0041 mov byte ptr [di+S],41 ;Copie A' dans S ).i J s8[l] 'B' ;.J j cs:OOAD A14400 mov ax,[I] ;Copie I en AX cs:OOBO 99 cwd et le convertir en Longlnt Li cs:OOB1 BF2000 mov di.0020 ;Charge en DI l'adresse des li-).J mites du domaine de validité Li cs:0084 9A1CO2E15F call RCheck ;Contrôle la validité j cs:00B9 8BF8 mov dl,ax ;Si Index 0K, le met en DI Li cs:OOBB C6854C0041 mov byte ptr [di+S8],42 ;Met 'B' dans S8 Li end;,J 'J begi ni w :- w + 1000,J cs:OODC A14600 mov ax,[W] ;W W + 1000 Li j cs:OODF 05E803 add ax,03E8 Li cs:00E2 31D2 xor dx,dx ;Mot de poids fort - O cs:00E4 BFC400 mcv di,00C4 ;Charge en DI l'adresse des li-),i mites du domaine de validité )_J cs:00E7 9A1CO2E15F cal] RCheck ;Contrôle la validité t cs:OOEC A34600 mov [W],ax ;Le nombre est 0K Li '-J I : i - 4711;.J cs:OOEF A14400 mov ax,[I] I :- I - 4711 Li t cs:00F2 2D6712 sub ax,1267 Li cs:OOFS 99 cwd ;Forme un entier long cs:00F6 BFCCOO mov di3OOCC ;Charge en DI l'adresse des li-Li mites du domaine de validité ).j cs:00F9 9A1CO2E15F call RCheck ;Contrôle la validité Li cs:OOFE A34400 mov [I],ax ;Le nombre est 0K Li 'J test ;J end. Le code qui conduit à l'interruption du programme ne se trouve pas dans ces séquences d'assembleur mais à l'intérieur de la routine RCheck qui appelle à cet effet la procédure standard RunError. Si RCheck revient au programme appelant, c'est que les limites du domaine de validité n'ont pas été franchies. L'opérande qui se trouve toujours dans les registres DX:AX peut alors être soumis au traitement désiré. L'utilisation du registre DI est ici particulièrement intéressante. Notez bien que le contenu de DI ne représente pas le type de la variable qui est exploité dans une sorte de clause CASE à l'intérieur de la routine RCheck. DI représente le déplacement de deux mots doubles DWORD qui indiquent la limite supérieure (maximum) et inférieure (minimum) du type concerné. Le listing suivant de la routine RCheck montre que ces limites ne sont pas stockées au même endroit que les constantes typées et les variables globales dans le segment de données, mais tout près de la procédure qui en a besoin, dans le segment de code. Dans les coulisses du langage 2-127 La Bible Turbo Pascal 2- 128 L'adresse du segment de code concerné est lue dans là pile, car dans un appel FAR le segment de code du programme appelant se trouve empilé. rcheck proc far.J j Entrées: DX:AX - OWORD à contrôler .J CS:[DI] - Minimum .J CS:[DI+4] - Maximum j j mov si.sp :Charge le pointeur de pile en SI-] mov es,ss:[si+02] ;Charge le segment de code de l'appelant-] en ES.J Compare DX:AX au minimum ES:[DI].J j cmp dx.es:[di+02] ;Compare DX au mot de poids fort j du minimum' jg super ;Supérieur 7 Oui --> Comparer au maximum Ji error ;Inférieur 7 Oui --> Erreur j cmp ax.es:[di] :Compare AX au mot de poids faible du minimum j jb error ;Inférieur 7 Oui --> Erreur j j Compare DX:AX au maximum ES:[DI+4]J j ;Compare DX au mot de poids fort j du maxinium.J super: cmp dx.es:[di-'-06] -Inférieur 7 Oui --> valeur o.k.j ;Supérieur 7 Oui --> Erreur j ;Egalité: Compare AX au mot de j il o poids faible du maximum j ;Supérieur 7 Oui --> Erreur j ;Valeur OK, retour à l'appelant j ;Message Range Check Error"j ;Interrompt le programme j error cmp ax.es:[di+04] j error 0k: retf error: mov ax,OOC9 jmp runerror rcheck end Les adresses des minima et des maxima sont donnés par le tableau ci-après. Chaque entrée du tableau, représentée par une ligne, prend 8 octets. Les optimisations effectuées pour les types Longlnt et les indices des chaînes sont remarquables. En effet, les deux premières lignes montrent que Turbo Pascal utilise la même adresse pour les types Byte et String qui évoluent tous deux dans l'intervalle 0-255. Les limites du type Longlnt sont données à l'adresse 0005h et s'intercalent en quelque sorte entre le type Byte et le type Shortint. Turbo Pascal ne prévoit pas d'entrée spéciale pour ce type. Cette particularité témoigne du haut niveau des programmeurs qui ont conçu Turbo Pascal. Adresse enmémoi centraIe .. Type... Interpréati4 ri décima..e Minimum Maximum' Minimum Maximum CS:0000 00000000 000000FF I3YTE 0 255 CS:0000 00000000 000000FF STRING 0 255 CS:0005 80000000 7FFFFFFF LONGINT -2147483648 2147483647 CS:0008 FFFFFF80 0000007F SHORTINT -128 127 CS:0010 00000000 00000001 BOOLEAN 0 1 CS:0018 00000000 0000000A Voiture 0 10 CS:0020 00000000 1 00000008 S8 0 8 2-128 La Bible Turbo Pascal 2- 129 Dans les coulisses du langage è ït Minimum Maximum Minimum Maximum CS:00C4 00000000 0000FFFF WORD 0 65536 CS:OOCC FFFF8000 00007FFF INTEGER -32768 32767 Il est d'autant plus étonnant que chaque procédure/ fonction construise son propre tableau indiquant les limites de tous les types de variables utilisés à l'intérieur de la procédure/ fonction. Comme le montre le programme suivant, Turbo Pascal ne se réfère pas aux tableaux générés par des procédures déjà compilées, mais il produit sans cesse de nouvelles entrées, même pour des types de données déjà traités par ailleurs. Cette façon de procéderparaît inefficace à première vue, mais elle est liée au fonctionnement interne de l'éditeur de liens lorsqu'il incorpore les unités au programme. Turbo Pascal dispose en effet d'un éditeur de liens intelligent qui ne prend en charge que les procédures, fonctions et variables réellement utilisées dans le programme. La décision d'incorporer ou non un élément ne peut cependant pas être prise au moment de la compilation, notamment à cause des unités. Une procédure P2 ne peut donc pas se référer au tableau des limites généré par PI car après tout il se pourrait que la procédure PI ne soit jamais appelée et qu'elle ne figure donc pas dans le fichier exécutable EXE. program rcheckl;.J J var I : integer;.J -J procedure tl;.J -J begi n.J

- j + 1;.J

( cs:0010 A13EO0 mov ax,[I] ;Copie I en AX )'J C cs:0020 40 inc ax ;Incrémente AX cs:0021 99 cwd ;et le convertit en Longlnt )J cs:0022 BF0000 mov di.0000 ;Charge en DI l'adresse des li-).J mites du domaine de validité )J cs:0025 9A1CO2985F cali RCheck ;Contrôle la validité cs:002A A33E00 mov [I],ax ;Résultat correct,le mémorise ).J -J end; -J procedure t2;J -J begi n.J

- I + 1;.J

cs:0038 A13EO0 mov ax,[I] ;Idem comme ci-dessus ( cs:003B 40 inc ax ; C cs:003C 99 cwd cs:003D 8F2600 mov di.0026 ;<-- mais adresse différente I )_J C cs:0040 9A1CO2985F cali RCheck ( cs:0045 A33E00 mov [I].ax ).J 'J end ;J -J begi nJ I + 1;J cs:0052 A13EO0 mov ax.[I] ;Toujours la même chose C cs:0055 40 inc ax ( cs:0056 99 cwd cs:0057 BF4COO mov dl.004C ;<-- mais l'adresse change I ).J Dans les coulisses du langage 2-129 La Bible Turbo Pascal 2-130 ( cs:005A 9A1CO2985F cail RCheck C cs:OOSF A33E00 mov [I].ax J ti ;_i t2 end. V Contrôle de la pile (Stack checking) Pour planter votre système, il n'y a rien de plus efficace et de plus désagréable qu'un débordement de pile. Ce genre d'incident survient lorsqu'on appelle des procédures et des fonctions, et que le pointeur de pile dépasse le début de la pile. Autrement dit, il n' y a plus assez de place pour stocker les variables locales de la procédure ou de la fonction appelée. Le contenu du pointeur de pile passe alors de O à $FFFF et adresse donc la fin du segment de pile. Comme Turbo Pascal, par défaut, ne réserve à l'intention de la pile, non pas un segment entier de mémoire mais uniquement 16 Ko, l'adresse pointée se trouve bien au-delà de la pile. Ce ne serait pas trop grave, si ce domaine n'était pas déjà utilisé par le tas. En règle générale, la crainte de cet incident ne doit pas vous obséder. Le danger n'est réel que dans les conditions suivantes: la taille de la pile a été réduite en-dessous des 16 Ko proposés par défaut, soit par la directive de compilation {$M} soit par l'option "Compiler/Memory Sizes" du menu "Options". • des types de variables volumineux, surtout des tableaux, sont transmis par valeur à une procédure /fonction, ce qui entraîne leur recopie sur la pile comme variables locales. • des procédures ou des fonctions s'appellent récursivement et la pile est consommée soit en raison du trop grand nombre d'appels, soit parce que les variables locales empilées à chaque appel sont trop nombreuses. Le contrôle de la pile, qui est enclenché par défaut, permet de détecter ce type d'erreur pendant la phase de test. Au début de chaque procédure/ fonction, Turbo Pascal appelle la routine de contrôle à laquelle est transmis, par l'intermédiaire du registre AX, le nombre d'octets nécessaires pour stocker les variables et les copies locales. La main n'est rendue au programme appelant que si le besoin en mémoire peut être satisfait par la pile compte tenu de sa position actuelle. Sinon, une routine interne est appelée qui provoque une erreur d'exécution et interrompt l'exécution du programme. Le listing suivant montre comment la routine de contrôle de la pile est invoquée: program stkchk;J J procedure test;J J var a : array [l..100] of word;J S string;.J -J begin.J C cs:0000 55 push bp ;Initialise la pile C cs:0001 89E5 mov bp.sp 2-130 La Bible Turbo Pascal 2- 131 Dans les coulisses du langage cs:0003 B8C801 mov ax.01C8 ;Nbre d'octets pris par les variables locales ) J cs:0006 9A4402935F cail far StkChk ;Appelle la routine de contrôle)..' cs:000B 81ECC801 sub sp.01C8 :11 y a de la place I J end;J cs:000F 89EC mov sp.bp ;Restaure la pile et efface )J cs:0011 50 pop bp ; les variables locales ( cs:0012 C3 ret j begin..J test ;J cs:001B E8E2FF cali test ;Appelle le test end. La procédure ci-dessus utilise deux variables locales appelées a et s. La variable a nécessite 200 octets de mémoire et s, en tant que chaîne standard, a besoin de 256 octets. La somme des deux est passée à la routine de contrôle StkChk par l'intermédiaire du registre AX. Ce n'est que si StkChk retourne à la procédure qu'il reste assez de place sur la pile et que les octets nécessaires (456) sont retranchés du registre SP. Mais comment fonctionne exactement le contrôle ? Comment détermine-t-il la disponibilité de la mémoire requise ? Le listing de StkChk donne quelques indications: proc far..' mov si.sp :Pointeur de pile en SI - nombre d'octets disponibles j sub si.ax ;Retranche les octets revendiqués Jb erreur ;( O 1 OUI --- > Erreur..' sub si.0200 ;Retranche encore 512 octets j ,jb erreur ;< O 1 OUI --- > Erreur j cmp si,[STACKLIMITJ ;Compare à la variable STACKLIMIT ,jb 0257 ;< STACKLIMIT? OUI --- > Erreur j ret ;Tout est OK. pas de débordement.J j erreur label nearJ j mov ax.00CA ;Code erreur: Stack-OverflowJ jmp runerror ;Provoque une erreur d'exécution j StkChk endp On voit que StkChk exige un supplément de mémoire par rapport au strict nécessaire. Après prise en compte des variables locales, il doit rester 512 octets sur la pile, et même davantage puisque StkChk demande encore de la place pour mémoriser la constante STACKLIMIT issue de l'unité System. Dans tous les tests effectués, cette variable gardait en fait la valeur O et n'influençait donc en rien le résultat de StkChk. Nous n'avons pas pu savoir si Turbo Pascal donnait parfois de lui-même une autre valeur à la variable, ou si la variable était destinée à être fixée par le programmeur. Le contrôle de pile ne dure pas très longtemps, mais il prend quand même un certain temps. On peut donc le désactiver par l'option "Co mpiler/Stack checking" du menu "Options". Les directives de compilation ($S+) et ($5-) permettent de déclencher sélectivement le contrôle en fonction de la procédure ou de la fonction. Il est en particulier recommandé d'activer le contrôle pour les sous-programmes récursifs, le niveau de récursion dépendant des conditions d'exécution et ne pouvant pas être déterminé au moment de la compilation. Dans les coulisses du langage 2-131 La Bible Turbo Pascal 2-132 V Contrôle des entrées-sorties (I/O checking) Chaque fois qu'un programme entre en communication avec l'extérieur pour adresser un périphérique ou un fichier, il risque de provoquer une erreur d'entrée-sortie. Imprimante manquant de papier, câble défectueux, disquette pleine, les motifs d'erreur ne manquent pas. Pour éviter des conséquences désastreuses, il faut détecter l'incident au plus tôt et trouver une parade appropriée. Turbo Pascal propose une solution générale à œ problème: le contrôle des entrées-sorties. Comme pour les contrôles précédents, Turbo Pascal insère un code spécial dans le programme compilé. Il est écrit après chaque appel d'une procédure ou d'une fonction de l'unité System qui utilise des ordres d'entrée-sortie. On trouve par exemple ce code après Writeln, Read ou Reset. Le contrôle est actif par défaut et conduit alors, en cas d'erreur, à l'interruption du programme avec un message d'erreur d'exécution. Mais cette démarche n'est pas toujours judicieuse. Si elle se veut conviviale, une application doit permettre à l'opérateur de remédier au défaut et de poursuivre le traitement. Un petit avertissement du genre "Allumez l'imprimante" ou "Introduisez la disquette" est déjà suffisant dans bien des cas. Pour permettre ce type d'intervention, on peut désactiver le contrôle général des entrées-sorties par l'option "Compiler/10 Checking" du menu "Options". Dans ce cas, plus aucun contrôle d'entrée-sortie n'est effectué dans le programme. Avec la directive de compilation ($1-), la désactivation, au lieu d'être générale, peut être limitée à une portion de programme ou à une procédure isolée. Le test d'erreur se fait alors en examinant la fonction lOResult qui est définie dans l'unité System. Avant de rendre la main, chaque instruction d'entrée-sortie détermine un code qui indique si l'entrée-sortie a été menée à bonne fin ou non. Ce code peut être consulté au moyen de la fonction lOResult. Le programme ci-dessous montre les différences entre un contrôle laissé à l'initiative de Turbo Pascal et un contrôle programmé individuellement sur mesure. program IoChk;J .J var fichiertest text;J Erreur : word;J J beginJ Assign( fichiertest. c:test.ioc );J cs:0031 BF3EO0 mov di.offset fichiertest ;Met sur la pile cs:0034 lE push ds ; un pointeur qui pointeLi cs:0035 57 push di ; sur la variable Li ( fichiertest L-i f cs:0036 BF0000 mov di .0000 ;Net sur la pile un pointeur Li cs:0039 0E push cs ; (CS:0000) qui pointe sur cs:003A 57 push di ; le nom du fichier 3.-i cs:003B 9A50029A5F call Assign ;Appelle la procédure Assign )J J Rewrite( fichiertest );.-i cs:0040 BF3EO0 mov di.offset fichiertest ;Met sur la pile un cs:0043 lE push ds pointeur qui pointe ).J cs:0044 57 push di sur la variable 3_i ( ; fichiertest 3.-i cs:0045 9AD2029A5F call Rewrite ;Appelle la procédure Rewrite 3_i f cs:004A 9A0E029A5F call IOChk ;Appelle le contrôle dE/5 3.i J ($I-) ( Désactive le contrôle automatique d'E/S)J 2-132 La Bible Turbo Pascal 2- 133 Dans les coulisses du langage writeln( fichiertest. 'Ecriture dans fichier');.J j cs:004F BF3EO0 mov di.offset fichiertest ;Transmet un polnteu r)J cs:0052 lE push ds sur la variable j cs:0053 57 push di fichiertest cs:0054 BFOCOO mov di,000C ;Empile un pointeur qui Li cs:0057 0E push cs pointe sur la chaîne à cs:0058 57 push di écrire cs:0059 31GO xor ax,ax ;Pas d'indication de longueur Li j cs:0058 50 push ax j cs:005C 9À26069A5F cail FStrArg ;Formate la chaîne j cs:0061 9AA9059A5F cail WritelnFlush ;Vide le tampon de sortie Li ;<-- Noter l'absence d'appel Li j à IOChk J Erreur :- IOResult;.J ( cs:0066 9A07029A5F cali IOResulat ;Apelle IORresult ).i cs:006B A33E01 mov [ERREUR],ax ;Mémorise le code erreur Li 'J if ( Erreur <> O ) then writeln( 'Erreur' );,J end. Ce programme accède à un fichier texte au moyen de la variable fichier fichiertest. La première instruction est un appel à la procédure Assign. Turbo Pascal ne déclenche aucun contrôle d'entrée-sortie car cette procédure est une procédure d'initialisation qui n'effectue ni entrée ni sortie. Il en est tout autrement de la procédure Rewrite qui génère ou efface un fichier. Quand cette procédure est terminée, Turbo Pascal appelle la routine de contrôle des entrées-sorties qui ne rend la main au programme qu'en l'absence d'erreur. L'instruction Writeln est précédée de la directive de compilation {$I-) qui désactive le contrôle des entrées-sorties. Le code=d'e oublie" alors d'insérer la routine. C'est le programme lui-même qui va se vérifier le bon fonctionnement des entrées-sorties. Après l'exécution de Writeln, la fonction IOResuIt est invoquée pour donner le code erreur de l'opération effectuée. Si la valeur est différente de O, il y a erreur et le programme réagit en conséquence en émettant un message d'erreur. Nous pouvons examiner le code que Turbo Pascal met en oeuvre dans IOChk pour tester le déroulement des entrées-sorties IOChk proc far.J 'J cmp word ptr [INOUTRES].0 :Compare le code erreur à O.J jne erreur ;<> O - --> Erreur-1 ret ;Parfait, pas d'erreur .J J erreur label near.J J mov ax.[INOUTRES] ;Charge le code erreur ,jmp runerror ;Interrompt le programme J IOChk endp La variable interne INOUTRES est une constante typée issue de l'unité System de type Word. C'est elle qui prend note du résultat des opérations d'entrée-sortie. La routine IOChk l'interroge pour transmettre son contenu à RunError lorsque sa valeur n'est pas nulle. Dans les coulisses du langage 2-133 La Bible Turbo Pascal 2- 134 Apparemment, le code erreur se trouve donc dans cette variable. Mais on peut alors se demander pourquoi IOResuit est une fonction et non simplement une référence à cette variable interne. Un coup d'oeil sur le code de lOResult fournit la réponse: lOResult proc far-1 xor ax.ax ; Met la valeur O dans AXJ xchg [INOUTRES],ax Echange les contenus de AX et INOUTRES. ret Retourne à 1 appelant.J lOResult endp Cette séquence n'a pas seulement pour tâche de retourner le code erreur mais en même temps elle remet à zéro la variable INOUTRES. Le manuel de Turbo Pascal explique clairement que seul le premier appel à lOResult fournit le code erreur de la dernière opération d'entrée-sortie et que les appels suivants renvoient systématiquement des zéros. Il reste que Turbo Pascal crée une fonction pour exécuter simplement deux instructions. L'appel FAR lui-même consomme beaucoup de temps et il occupe presque autant de place que les instructions appelées. Cette méthode n'est pas optimale, les dispositions prises pour insérer directement le code des appels à PTR, CHR ou INC le montrent clairement. 2-134 La Bible Turbo Pascal