Manuel de suivi dynamique Solaris

Chapitre 5 Pointeurs et ensembles

Pointeurs et adresses mémoire d'objets de données dans le noyau du système d'exploitation ou dans l'espace d'adresse d'un processus utilisateur. D offre la possibilité de créer et de manipuler des pointeurs et de les stocker dans des variables et des ensembles associatifs. Ce chapitre décrit la syntaxe D de pointeurs, d'opérateurs qui peuvent être appliqués pour créer et accéder à des pointeurs, ainsi que la relation entre des pointeurs et des ensembles scalaires de taille fixe. Des problèmes liés à l'utilisation de pointeurs dans différents espaces d'adresse sont également abordés.


Remarque –

Si vous êtes un programmeur C ou C++ expérimenté, vous pouvez survoler ce chapitre car la syntaxe de pointeur D est identique à la syntaxe ANSI-C correspondante. Nous vous conseillons de lire les sections Pointeurs sur des objets DTrace et Pointeurs et espaces d'adresse, car elles présentent les fonctions et les problèmes spécifiques à DTrace.


Pointeurs et adresses

Le système d'exploitation Solaris utilise une technique appelée mémoire virtuelle pour fournir à chaque processus utilisateur un affichage virtuel propre des ressources mémoire du système. Un affichage virtuel des ressources mémoire est appelé espace d'adresse, qui associe une plage de valeurs d'adresses ([0 ... 0xffffffff] pour un espace d'adresse 32 bits ou [0 ... 0xffffffffffffffff] pour un espace d'adresse 64 bits) avec un ensemble de translations utilisé par le système d'exploitation et le matériel pour convertir chaque adresse virtuelle en un emplacement de mémoire physique correspondant. Les pointeurs dans D sont des objets de données qui stockent une valeur d'adresse virtuelle entière et l'associe à un type D qui décrit le format des données stockées à l'emplacement de mémoire correspondant.

Vous pouvez déclarer une variable D de sorte qu'elle soit un type de pointeur en spécifiant tout d'abord le type des données référencées, puis en ajoutant un astérisque ( *) au nom du type pour indiquer que vous souhaitez déclarer un pointeur. Par exemple, la déclaration :

int *p;

déclare une variable globale D nommée p qui est un pointeur sur un entier. Cette déclaration signifie que p est un entier de 32 ou 64 bits dont la valeur est l'adresse d'un autre entier en mémoire. La forme compilée de votre code D étant exécutée au déclenchement de sonde dans le noyau du système d'exploitation, les pointeurs D sont généralement associés à l'espace d'adresse du noyau. Vous pouvez utiliser la commande isainfo(1) -b pour déterminer le nombre de bits utilisés pour les pointeurs par le noyau du système d'exploitation actif.

Pour créer un pointeur sur un objet de données dans le noyau, vous pouvez calculer son adresse à l'aide de l'opérateur &. Par exemple, le code source de noyau du système d'exploitation déclare un paramètre réglable int kmem_flags. Vous pourriez suivre l'adresse de int en suivant le résultat de l'application de l'opérateur & au nom de cet objet dans D :

trace(&`kmem_flags);

L'opérateur * peut être utilisé pour faire référence à l'objet pointé et agit de manière inverse à l'opérateur &. Par exemple, les deux fragments de code D ont une signification équivalente :

p = &`kmem_flags;				trace(`kmem_flags);
trace(*p);

Le fragment de gauche crée un pointeur p de variable globale D. L'objet kmem_flags étant du type int, le type du résultat de &`kmem_flags est int * (à savoir le pointeur sur int). Le fragment de gauche suit la valeur de *p, qui suit le pointeur sur l'objet de données kmem_flags. Ce fragment est donc identique à celui de droite, qui suit simplement la valeur de l'objet de données via son nom.

Sécurité du pointeur

Si vous êtes un programmeur C ou C++, vous pouvez être quelque peu inquiet après avoir lu la section précédente car vous savez qu'une mauvaise utilisation de pointeurs dans vos programmes peut entraîner leur panne. DTrace est un environnement puissant et sécurisé permettant d'exécuter vos programmes D là où des erreurs ne peuvent pas entraîner de panne des programmes. Vous pouvez en effet rédiger un programme D avec bogue, mais les accès d'un pointeur D non valide n'entraîneront pas une défaillance ou une panne de DTrace ou du noyau du système d'exploitation. Le logiciel DTrace détectera plutôt tous les accès de pointeur non valides, désactivera votre instrumentation, et vous signalera le problème pour débogage.

Si vous avez programmé en langage Java, vous savez probablement que ce langage ne prend pas en charge les pointeurs pour ces mêmes raisons de sécurité. Des pointeurs sont nécessaires dans D car ils font intrinsèquement partie de la mise en œuvre du système d'exploitation dans C, mais DTrace met en œuvre le même type de mécanisme de sécurité rencontré dans le langage de programmation Java empêchant une détérioration des programmes avec bogue eux-mêmes ou les uns par rapport aux autres. Les rapports d'erreurs DTrace sont similaires à l'environnement d'exécution du langage de programmation Java qui détecte une erreur de programmation et qui vous signale une exception.

Pour consulter la gestion et les rapports d'erreurs DTrace, rédigez un programme D incorrect utilisant des pointeurs. Dans un éditeur, entrez le programme D suivant et enregistrez-le sous un fichier nommé badptr.d :


Exemple 5–1 badptr.d : démonstration de la gestion d'erreurs DTrace

BEGIN
{
	x = (int *)NULL;
	y = *x;
	trace(y);
}

Le programme badptr.d crée un pointeur D nommé x qui pointe sur int. Le programme assigne à ce pointeur la valeur spéciale de pointeur non valide NULL, qui est un alias intégré pour l'adresse 0. Par convention, l'adresse 0 est toujours définie comme non valide, de façon à ce que NULL soit utilisé en tant valeur sentinelle dans les programmes en langages C et D. Le programme utilise une expression de forçage de type pour convertir NULL en pointeur sur un entier. Le programme déréférence alors le pointeur à l'aide de l'expression *x et attribue le résultat à une autre variable y, puis tente de suivre y. Une fois le programme D exécuté, DTrace détecte un accès de pointeur non valide lorsque l'instruction y = *x est exécutée et signale l'erreur suivante :


# dtrace -s badptr.d
dtrace: script '/dev/stdin' matched 1 probe
CPU     ID                    FUNCTION:NAME
dtrace: error on enabled probe ID 1 (ID 1: dtrace:::BEGIN): invalid address
(0x0) in action #2 at DIF offset 4
dtrace: 1 error on CPU 0
^C
#

L'autre problème pouvant survenir des programmes utilisant des pointeurs non valides est une erreur d'alignement. Par convention architecturale, des objets de données fondamentaux comme des entiers sont alignés dans la mémoire en fonction de leur taille. Par exemple, des entiers 2 octets sont alignés sur des adresses qui sont des multiples d'entiers 2, 4 octets par multiples de 4, etc. Si vous déréférencez un pointeur sur un entier 4 octets et que votre adresse de pointeur est une valeur non valide autre qu'un multiple de 4, votre accès échouera et renverra une erreur d'alignement. Les erreurs d'alignement dans D indiquent quasiment toujours que votre pointeur comporte une valeur non valide ou corrompue en raison d'un bogue dans votre programme D. Vous pouvez créer un exemple d'erreur d'alignement en changeant le code source de badptr.d pour utiliser l'adresse (int *)2 plutôt que NULL. int étant de 4 octets et 2 n'étant pas un multiple de 4, l'expression *x entraîne une erreur d'alignement DTrace.

Pour plus d'informations sur le mécanisme d'erreur DTrace, reportez-vous à la section Sonde ERROR.

Déclarations et stockage d'ensembles

Outre les tableaux associatifs dynamiques présentés dans le chapitre 3, D prend également en charge les tableaux scalaires. Les tableaux scalaires sont un groupe de longueur fixe contenant des emplacements mémoire consécutifs, chacun d'eux stockant une valeur du même type. Les ensembles scalaires sont accessibles en référençant chaque emplacement avec un entier à partir de zéro. Les ensembles scalaires correspondent au concept et à la syntaxe des ensembles dans C et C++. Les tableaux scalaires ne sont pas utilisés aussi fréquemment dans D que les tableaux associatifs et leurs groupements plus avancés, mais ils sont parfois nécessaires pour accéder aux structures de données de tableau du système d'exploitation existantes déclarées dans C. Les groupements sont décrits dans le Chapitre9Groupements.

Un ensemble scalaire D de 5 entiers pourrait être déclaré à l'aide du type int et en suffixant la déclaration par le nombre d'éléments entre crochets comme suit :

int a[5];

Le diagramme suivant illustre une représentation visuelle du stockage d'ensembles :

Figure 5–1 Représentation d'ensembles scalaires

Le diagramme illustre un ensemble de cinq objets.

L'expression D a[0] permet de référencer le premier élément de l'ensemble, a[1] le deuxième, etc. D'un point de vue syntaxique, les ensembles scalaires et associatifs sont très similaires. Vous pouvez déclarer un ensemble associatif de cinq entiers référencé par un entier comme suit :

int a[int];

et référencer également cet ensemble à l'aide de l'expression a[0]. Cependant, du point de vue stockage et mise en œuvre, les deux ensembles sont très différents. L'ensemble statique a comprend cinq emplacements mémoire consécutifs à partir de zéro et l'index fait référence à un décalage dans le stockage attribué à l'ensemble. D'autre part, un ensemble associatif ne dispose pas de taille prédéfinie et ne stocke pas d'éléments dans des emplacements mémoire consécutifs. De plus, les clés d'ensemble associatif n'ont aucune relation avec l'emplacement de stockage de valeur correspondante. Vous pouvez accéder aux éléments d'ensemble associatif a[0] et a[-5] et deux mots de stockage seulement seront attribués par DTrace, ceux-ci pouvant être consécutifs ou non. Les clés d'ensemble associatif sont des noms abstraits de la valeur correspondante sans relation avec les emplacements de stockage de valeur.

Si vous créez un ensemble à l'aide d'une affectation initiale et que utilisez une seule expression d'entier comme index d'ensemble (par exemple, a[0] = 2), le compilateur D crée toujours un ensemble associatif, même si cette expression a peut également être interprétée comme une affectation d'ensemble scalaire. Les ensembles scalaires doivent être prédéclarés dans ce cas pour que le compilateur D puisse afficher la définition de taille de l'ensemble et en déduire que l'ensemble est un ensemble scalaire.

Relation entre pointeur et ensemble

Les pointeurs et ensembles entretiennent une relation spéciale dans D, tout comme dans ANSI-C. Un ensemble est représenté par une variable associée à l'adresse de son premier emplacement de stockage. Un pointeur représente également l'adresse d'un emplacement de stockage d'un type défini. D permet donc d'utiliser la notation d'index d'ensemble [ ] avec des variables de pointeur et des variables d'ensemble. Par exemple, les deux fragments D suivants ont une signification équivalente :

p = &a[0];				trace(a[2]);
trace(p[2]);

Dans le fragment de gauche, le pointeur p est affecté à l'adresse du premier élément de l'ensemble dans a en appliquant l'opérateur & à l'expression a[0]. L'expression p[2] suit la valeur du troisième élément (index 2) de l'ensemble. p contenant désormais la même adresse associée à a, cette expression donne la même valeur que a[2], tel qu'illustré dans le fragment de droite. Une conséquence de cette équivalence est que C et D vous permettent d'accéder à tout index de tout pointeur ou ensemble. La vérification des liaisons d'ensemble n'est pas exécutée par le compilateur ou l'environnement d'exécution DTrace pour vous. Si vous accédez à la mémoire après la fin d'une valeur prédéfinie d'un ensemble, vous obtiendrez un résultat inattendu ou DTrace signalera une erreur d'adresse non valide, tel qu'illustré dans l'exemple précédent. Comme toujours, vous ne pouvez pas détériorer DTrace ou le système d'exploitation, mais vous devrez déboguer le programme D.

La différence entre les pointeurs et les ensembles est telle qu'une variable de pointeur fait référence à un stockage distinct contenant l'adresse d'entier d'un autre stockage. Un variable d'ensemble nomme le stockage de l'ensemble, et non l'emplacement d'un entier contenant à son tour l'emplacement de l'ensemble. Cette différence est illustrée dans le diagramme suivant :

Figure 5–2 Stockage de pointeur et d'ensemble

Le diagramme illustre le pointeur d'un ensemble de cinq objets.

Cette différence est représentée dans la syntaxe D si vous tentez d'affecter des pointeurs et des ensembles scalaires. Si x et y sont des variables de pointeur, l'expression x = y est légale. Elle copie simplement l'adresse de pointeur dans y vers l'emplacement de stockage nommé par x. Si x et y sont des variables d'ensemble scalaire, l'expression x = y n'est pas légale. Des ensembles peuvent ne pas être intégralement affectés dans D. Cependant, une variable d'ensemble ou un nom de symbole peut être utilisé au cas où un pointeur serait autorisé. Si p est un pointeur et que a est un ensemble, l'instruction p = a est autorisée. Celle-ci équivaut à l'instruction p = &a[0].

Arithmétique de pointeur

Les pointeurs n'étant que des entiers utilisés comme des adresses d'autres objets en mémoire, D propose un ensemble de fonctions permettant d'exécuter une arithmétique sur des pointeurs. L'arithmétique de pointeur n'est cependant pas identique à l'arithmétique d'entier. L'arithmétique de pointeur définit implicitement l'adresse sous-jacente en multipliant ou en divisant les opérandes par la taille du type référencé par le pointeur. Le fragment D suivant illustre cette propriété :

int *x;

BEGIN
{
	trace(x);
	trace(x + 1);
	trace(x + 2);
}

Ce fragment crée un pointeur entier x, puis suit sa valeur, incrémentée de un, et sa valeur incrémentée de deux. Si vous créez et exécutez ce programme, DTrace signale les valeurs d'entier 0, 4 et 8.

x étant un pointeur sur un entier (4 octets), l'incrémentation de x ajoute 4 à la valeur de pointeur sous-jacente. Cette propriété est utile en cas d'utilisation de pointeur pour faire référence à des emplacements de stockage consécutifs comme des ensembles. Par exemple, si x était affecté à l'adresse d'un tableau a, similaire à celui illustré dans la Figure 5–2, l'expression x + 1 serait équivalente à l'expression &a[1]. De même, l'expression *(x + 1) ferait référence à la valeur a[1]. L'arithmétique de pointeur est mise en œuvre par le compilateur D si une valeur de pointeur est incrémentée à l'aide de l'opérateur +=, + ou ++.

L'arithmétique de pointeur s'applique également lorsqu'un entier est soustrait d'un pointeur à gauche, lorsqu'un pointeur est soustrait d'un autre pointeur ou lorsque l'opérateur -- est appliqué à un pointeur. Par exemple, le programme D suivant suivrait le résultat 2 :

int *x, *y;
int a[5];

BEGIN
{
	x = &a[0];
	y = &a[2];
	trace(y - x);
}

Pointeurs génériques

Il est parfois utile de représenter ou de manipuler une adresse de pointeur générique dans un programme D sans spécifier le type de données référencées par le pointeur. Des pointeurs génériques peuvent être spécifiés à l'aide du type void *, où le mot-clé void représente l'absence d'informations de type spécifiques, ou à l'aide de l'alias de type intégré uintptr_t qui représente un alias de type de taille entier non signé approprié d'un pointeur dans le modèle de données en cours. Vous ne pouvez pas appliquer l'arithmétique de pointeur à un objet du type void *, et ces pointeurs ne peuvent pas être déréférencés sans qu'ils aient été tout d'abord forcés à un autre type. Vous pouvez forcer le type d'un pointeur sur uintptr_t en cas d'exécution nécessaire d'arithmétique d'entier sur la valeur de pointeur.

Les pointeurs sur void peuvent être utilisés lorsqu'un autre type de données pour un pointeur est nécessaire, comme une expression de tuple d'ensemble associatif ou à droite d'une instruction d'affectation. De même, un pointeur sur un quelconque type de données peut être utilisé lorsqu'un pointeur sur void est nécessaire. Pour utiliser un pointeur de type non-void à la place d'un autre type de pointeur non-void, un forçage de type explicite est nécessaire. Vous devez toujours utiliser des forçages de type explicites pour convertir des pointeurs en types d'entier comme uintptr_t ou pour reconvertir ces entiers dans le type de pointeur approprié.

Ensembles multidimensionnels

Les ensembles scalaires multidimensionnels ne sont pas utilisés fréquemment dans D, mais sont proposés pour une compatibilité avec ANSI-C et pour observer et accéder à des structures de données système créées à l'aide de cette fonctionnalité dans C. Un ensemble multidimensionnel est déclaré comme une série consécutive de tailles d'ensembles scalaires comprise entre crochets [ ] et suivie du type de base. Par exemple, pour déclarer un ensemble rectangulaire à deux dimensions de taille fixe dont les dimensions sont de 12 lignes par 34 colonnes, rédigez la déclaration suivante :

int a[12][34];

Un ensemble scalaire multidimensionnel est accessible via une notation similaire. Par exemple, pour accéder à la valeur stockée à la ligne 0, colonne 1, rédigez l'expression D suivante :

a[0][1]

Les emplacements de stockage des valeurs d'ensemble scalaire multidimensionnel sont calculés en multipliant le nombre de lignes par le nombre total de colonnes déclarées, puis en ajoutant le nombre de colonnes.

Veillez à ne pas confondre la syntaxe d'ensemble multidimensionnel avec la syntaxe D d'accès d'ensemble associatif (à savoir que a[0][1] est différent de a[0, 1]). Si vous utilisez un tuple incompatible avec un ensemble associatif ou si vous tentez un accès d'ensemble associatif d'un ensemble scalaire, le compilateur D renverra un message d'erreur et ne compilera pas votre programme.

Pointeurs sur des objets DTrace

Le compilateur D vous empêche d'utiliser l'opérateur & afin d'obtenir des pointeurs sur des objets DTrace comme des ensembles associatifs, des fonctions intégrées et des variables. Vous ne pouvez pas obtenir l'adresse de ces variables de sorte que l'environnement d'exécution DTrace est libre de les relocaliser si nécessaire entre des déclenchements de sonde afin de gérer plus efficacement la mémoire nécessaire aux programmes. Si vous créez des structures composites, il est possible de développer des expressions permettant de récupérer l'adresse du noyau de votre stockage d'objet DTrace. Vous ne devez pas créer de telles expressions dans vos programmes D. Si vous devez utiliser une telle expression, veillez à ne pas mettre l'adresse en cache entre les déclenchements de sonde.

Dans ANSI-C, les pointeurs peuvent également être utilisés pour exécuter des appels de fonction indirects ou des affectations, comme le placement d'une expression à l'aide de l'opérateur de déréférencement unaire * à gauche d'un opérateur d'affectation. Dans D, ces types d'expression utilisant des pointeurs ne sont pas autorisés. Vous ne pouvez affecter des valeurs directement à des variables D que via leur nom ou en appliquant l'opérateur d'index d'ensemble [] à un ensemble scalaire ou associatif D. Vous pouvez uniquement appeler des fonctions définies par l'environnement DTrace par leur nom, tel que spécifié dans le Chapitre10Actions et sous-routines Les appels de fonction indirects utilisant des pointeurs ne sont pas autorisés dans D.

Pointeurs et espaces d'adresse

Un pointeur est une adresse proposant une translation dans un espace d'adresse virtuelle vers un emplacement de la mémoire physique. DTrace exécute vos programmes D dans l'espace d'adresse virtuelle du noyau du système d'exploitation. Votre système Solaris gère de nombreux espaces d'adresse : un pour le noyau du système d'exploitation et un pour chaque processus utilisateur. Chaque espace d'adresse semblant pouvoir accéder à l'ensemble de la mémoire du système, la même valeur de pointeur d'adresse virtuelle peut être réutilisée pour d'autres espaces d'adresse mais refléter une autre mémoire physique. Par conséquent, lors de la rédaction de programmes D utilisant des pointeurs, vous devez connaître l'espace d'adresse correspondant aux pointeurs que vous souhaitez utiliser.

Par exemple, si vous utilisez le fournisseur syscall pour instrumentaliser l'entrée d'un appel système utilisant un pointeur sur un entier ou un ensemble d'entiers comme argument (par exemple, pipe(2)), il ne serait pas approprié de déréférencer ce pointeur ou cet ensemble à l'aide de l'opérateur * ou [], car l'adresse concernée est une adresse contenue dans l'espace d'adresse du processus utilisateur à l'origine de l'appel système. L'application de l'opérateur * ou [] à cette adresse dans D entraînerait un accès à l'espace d'adresse du noyau, causant ainsi une erreur d'adresse ou l'obtention de données inattendues dans votre programme D si l'adresse devait correspondre à une adresse de noyau valide.

Pour accéder à la mémoire de processus utilisateur à partir d'une sonde DTrace, vous devez appliquer l'une des fonctions copyin(), copyinstr() ou copyinto(), décrites dans le Chapitre10Actions et sous-routines, au pointeur d'espace d'adresse utilisateur. Lors de la rédaction de programmes D, veillez à nommer et à commenter les variables en stockant de manière appropriée les adresses utilisateur pour éviter toute confusion. Vous pouvez également stocker des adresses utilisateur sous uintptr_t afin de ne pas compiler par inadvertance le code D les déréférençant. Les techniques d'utilisation de DTrace sur des processus utilisateur sont présentées dans le Chapitre33Suivi des processus utilisateur.