Le pointeur
fait référence à une cellule de mémoire, et
déréférencer un pointeur signifie lire la valeur de la cellule spécifiée. La valeur du pointeur lui-même est l'adresse de la cellule mémoire. La norme de langage C ne spécifie pas la forme de représentation des adresses mémoire. Il s'agit d'un point très important, car différentes architectures peuvent utiliser différents modèles d'adressage. La plupart des architectures modernes utilisent un espace d'adressage linéaire ou similaire. Cependant, même cette question n'est pas spécifiée strictement, car les adresses peuvent être physiques ou virtuelles. Certaines architectures utilisent une représentation non numérique du tout. Ainsi, Symbolics Lisp Machine fonctionne avec des tuples de la forme
(objet, offset) comme adresses.
Quelque temps plus tard, après la publication de la traduction sur Habré, l'auteur a apporté d'importantes modifications au texte de l'article. Mettre à jour une traduction sur Habré n'est pas une bonne idée, car certains commentaires perdront leur sens ou sembleront déplacés. Je ne veux pas publier le texte en tant que nouvel article. Par conséquent, nous venons de mettre à jour la traduction de l'article sur viva64.com, et ici nous avons tout laissé tel quel. Si vous êtes un nouveau lecteur, je vous suggère de lire une traduction plus récente sur notre site en cliquant sur le lien ci-dessus. |
La norme ne précise pas la forme de présentation des pointeurs, mais stipule - dans une plus ou moins grande mesure - les opérations avec eux. Ci-dessous, nous considérons ces opérations et les caractéristiques de leur définition dans la norme. Commençons par l'exemple suivant:
#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; }
Si nous compilons ce code GCC avec le niveau d'optimisation 1 et exécutons le programme sous Linux x86-64, il affichera ce qui suit:
0x7fff4a35b19c 0x7fff4a35b19c 0
Notez que les pointeurs
p et
q font référence à la même adresse. Cependant, le résultat de l'expression
p == q est
faux , et cela à première vue semble étrange. Les deux pointeurs vers la même adresse ne devraient-ils pas être égaux?
Voici comment la norme C définit le résultat de la vérification de l'égalité de deux pointeurs:
C11 § 6.5.9 paragraphe 6
Deux pointeurs sont égaux si et seulement si les deux sont nuls, soit pointent vers le même objet (y compris un pointeur vers l'objet et le premier sous-objet dans l'objet) ou une fonction, soit pointent vers la position après le dernier élément du tableau, ou un pointeur fait référence à la position après le dernier élément du tableau, et l'autre fait référence au début d'un autre tableau suivant immédiatement le premier dans le même espace d'adressage. |
Tout d'abord, la question se pose: qu'est-ce qu'un «objet
» ? Puisque nous parlons du langage C, il est évident qu'ici les objets n'ont rien à voir avec les objets dans les langages OOP comme C ++. Dans la norme C, ce concept n'est pas complètement défini:
C11 § 3.15
Un objet est une zone de stockage d'exécution dont le contenu peut être utilisé pour représenter des valeurs
REMARQUE Lorsqu'il est mentionné, un objet peut être considéré comme ayant un type spécifique; voir 6.3.2.1. |
Faisons les choses correctement. Une variable entière 16 bits est un ensemble de données en mémoire qui peut représenter des valeurs entières 16 bits. Par conséquent, une telle variable est un objet. Est-ce que deux pointeurs seront égaux si l'un d'eux fait référence au premier octet d'un entier donné et le second au deuxième octet du même nombre? Le comité de normalisation linguistique, bien sûr, ne le voulait pas du tout. Mais ici, il convient de noter qu'à cet égard, il n'a pas d'explications claires, et nous sommes obligés de deviner ce que l'on voulait vraiment dire.
Quand le compilateur se met en travers
Revenons à notre premier exemple. Le pointeur
p est obtenu à partir de l'objet
a , et le pointeur
q est à partir de l'objet
b . Dans le second cas, l'arithmétique d'adresse est utilisée, qui est définie comme suit pour les opérateurs plus et moins:
C11 § 6.5.6 clause 7
Lorsqu'il est utilisé avec ces opérateurs, un pointeur sur un objet qui n'est pas un élément du tableau se comporte comme un pointeur sur le début d'un tableau d'une longueur d'un élément, dont le type correspond au type de l'objet d'origine. |
Étant donné que tout pointeur vers un objet qui n'est pas un tableau devient en
fait un pointeur vers un tableau d'une longueur d'un élément, la norme définit l'arithmétique des adresses uniquement pour les pointeurs vers les tableaux - c'est le point 8. Nous nous intéressons à la partie suivante:
C11 § 6.5.6 clause 8
Si une expression entière est ajoutée ou soustraite du pointeur, le pointeur résultant est du même type que le pointeur d'origine. Si le pointeur source fait référence à un élément de tableau et que le tableau est de longueur suffisante, la source et les éléments résultants sont séparés les uns des autres de sorte que la différence entre leurs indices soit égale à la valeur de l'expression entière. En d'autres termes, si l'expression P pointe vers le ième élément du tableau, les expressions (P) + N (ou son équivalent N + (P) ) et (P) -N (où N a la valeur n) indiquent respectivement (i + n) e et (i - n) e éléments du tableau, à condition qu'ils existent. De plus, si l'expression P pointe vers le dernier élément du tableau, alors l'expression (P) +1 indique la position après le dernier élément du tableau, et si l'expression Q indique la position après le dernier élément du tableau, alors l'expression (Q) -1 indique le dernier élément tableau. Si la source et les pointeurs résultants font référence à des éléments du même tableau ou à la position après le dernier élément du tableau, le débordement est exclu; sinon, le comportement n'est pas défini. Si le pointeur résultant fait référence à la position après le dernier élément du tableau, l'opérateur unaire * ne peut pas lui être appliqué. |
Il s'ensuit que le résultat de l'expression
& b + 1 devrait certainement être une adresse, et donc
p et
q sont des pointeurs valides. Permettez-moi de vous rappeler comment l'égalité de deux pointeurs dans la norme est définie: "
Deux pointeurs sont égaux si et seulement si [...] un pointeur se réfère à la position après le dernier élément du tableau, et l'autre au début d'un autre tableau immédiatement après le premier du même espace d'adressage " (C11 § 6.5.9, clause 6). C'est exactement ce que nous observons dans notre exemple. Le pointeur q fait référence à la position après l'objet b, immédiatement suivie par l'objet a, auquel le pointeur p fait référence. Alors, y a-t-il un bug dans GCC? Cette contradiction a été décrite en 2014 comme le
bug # 61502 , mais les développeurs de GCC ne le considèrent pas comme un bug et ne vont donc pas le corriger.
Un problème similaire a été rencontré en 2016 par les programmeurs Linux. Considérez le code suivant:
extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { } }
Les symboles
_start et
_end spécifient les limites de la zone mémoire. Puisqu'ils sont transférés vers un fichier externe, le compilateur ne sait pas comment les tableaux sont réellement situés en mémoire. Pour cette raison, il doit être prudent ici et partir de l'hypothèse qu'ils se succèdent dans l'espace d'adressage. Cependant, GCC compile la condition de boucle afin qu'elle soit toujours vraie, ce qui rend la boucle infinie. Ce problème est décrit ici dans cet
article sur LKML - un fragment de code similaire y est utilisé. Il semble que dans ce cas, les auteurs de GCC aient néanmoins pris en compte les commentaires et changé le comportement du compilateur. Au moins, je n'ai pas pu reproduire cette erreur dans GCC version 7.3.1 sous Linux x86_64.
Solution - dans le rapport de bogue n ° 260?
Notre cas peut clarifier le rapport de bogue
# 260 . Il s'agit plutôt de valeurs incertaines, mais vous pouvez y trouver un commentaire curieux du comité:
Les implémentations du compilateur [...] peuvent également distinguer les pointeurs obtenus à partir d'objets différents, même si ces pointeurs ont le même ensemble de bits.Si nous prenons ce commentaire à la lettre, alors il est logique que le résultat de l'expression
p == q soit «faux», car
p et
q sont obtenus à partir d'objets différents qui ne sont connectés d'aucune façon. Il semble que nous nous rapprochions de la vérité - ou non? Jusqu'à présent, nous avons traité des opérateurs d'égalité, mais qu'en est-il des opérateurs de relation?
Le dernier indice est en relation avec les opérateurs?
La définition des opérateurs de relation
< ,
<= ,
> et
> = dans le contexte des comparaisons de pointeurs contient une pensée curieuse:
C11 § 6.5.8 paragraphe 5
Le résultat de la comparaison de deux pointeurs dépend de la position relative des objets indiqués dans l'espace d'adressage. Si deux pointeurs vers des types d'objet font référence au même objet, ou les deux font référence à la position après le dernier élément du même tableau, ces pointeurs sont égaux. Si les objets indiqués sont membres du même objet composite, les pointeurs vers les membres de la structure déclarés plus tard sont plus que les pointeurs vers les membres déclarés plus tôt, et les pointeurs vers les éléments d'un tableau avec des indices plus élevés sont plus que les pointeurs vers les éléments du même tableau avec des indices inférieurs. Tous les pointeurs vers les membres d'une même association sont égaux. Si l'expression P pointe vers un élément du tableau et que l'expression Q pointe vers le dernier élément du même tableau, alors la valeur de l'expression de pointeur Q + 1 est supérieure à la valeur de l'expression P. Dans tous les autres cas, le comportement n'est pas défini. |
Selon cette définition, le résultat de la comparaison des pointeurs n'est déterminé que si les pointeurs sont obtenus à partir
du même objet. Nous montrons cela avec deux exemples.
int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q)
Ici, les pointeurs
p et
q font référence à deux objets différents qui ne sont pas interconnectés. Par conséquent, le résultat de leur comparaison n'est pas défini. Mais dans l'exemple suivant:
int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo();
les pointeurs
p et
q se réfèrent au même objet et sont donc interconnectés. Ainsi, ils peuvent être comparés - à moins que
malloc ne renvoie une valeur nulle.
Résumé
La norme C11 ne décrit pas correctement les comparaisons de pointeurs. Le point le plus problématique que nous avons rencontré était le paragraphe 6 § 6.5.9, où il est explicitement autorisé de comparer deux pointeurs qui font référence à deux tableaux différents. Cela contredit le commentaire du rapport de bogue n ° 260. Cependant, nous parlons là de significations indéfinies, et je ne voudrais pas construire mon raisonnement sur la seule base de ce commentaire et l'interpréter dans un autre contexte. Lors de la comparaison de pointeurs, les opérateurs de relation sont définis légèrement différemment des opérateurs d'égalité - à savoir, les opérateurs de relation ne sont définis que si les deux pointeurs sont obtenus à partir
du même objet.
Si nous ignorons le texte de la norme et demandons s'il est possible de comparer deux pointeurs obtenus à partir de deux objets différents, alors dans tous les cas la réponse sera très probablement «non». L'exemple au début de l'article montre un problème théorique. Étant donné que les variables
a et
b ont des durées de stockage automatique, nos hypothèses sur leur placement en mémoire ne seront pas fiables. Dans certains cas, nous pouvons le deviner, mais il est évident qu'un tel code ne peut pas être porté en toute sécurité, et vous ne pouvez découvrir la signification du programme qu'en compilant et en exécutant ou en désassemblant le code, ce qui contredit tout paradigme de programmation sérieux.
Cependant, en général, je ne suis pas satisfait du libellé de la norme C11, et comme plusieurs personnes ont déjà rencontré ce problème, la question demeure: pourquoi ne pas formuler plus clairement les règles?
Addition
Pointe vers la position après le dernier élément du tableau
En ce qui concerne la règle de comparaison et d'adressage de l'arithmétique des pointeurs à la position après le dernier élément du tableau, vous pouvez souvent y trouver des exceptions. Supposons que la norme ne permette pas de comparer deux pointeurs obtenus à partir
du même tableau, même si au moins l'un d'eux fait référence à la position au-delà de la fin du tableau. Ensuite, le code suivant ne fonctionnerait pas:
const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { }
En utilisant une boucle, nous parcourons tout le tableau
x , composé de 64 éléments, c'est-à-dire le corps de la boucle doit s'exécuter exactement 64 fois. Mais en fait, la condition est vérifiée 65 fois - une fois de plus que le nombre d'éléments dans le tableau. Dans les 64 premières itérations, le pointeur
i se réfère toujours à l'intérieur du tableau
x , tandis que l'expression
& x [num] indique toujours la position après le dernier élément du tableau. À la 65e itération, le pointeur
i fera également référence à la position au-delà de la fin du tableau
x , à cause de laquelle la condition de boucle devient fausse. Il s'agit d'un moyen pratique de contourner l'ensemble du tableau et il repose sur une exception à la règle d'incertitude de comportement lors de la comparaison de ces pointeurs. Notez que la norme décrit uniquement le comportement lors de la comparaison de pointeurs; le déréférencement est une question distincte.
Est-il possible de changer notre exemple afin qu'aucun pointeur ne fasse référence à la position après le dernier élément du tableau
x ? C'est possible, mais ce sera plus difficile. Nous devrons changer la condition de la boucle et interdire l'incrément de la variable
i à la dernière itération.
const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { if (i == &x[num-1]) break; }
Ce code est plein de subtilités techniques, qui agitent ce qui détourne de la tâche principale. De plus, une branche supplémentaire est apparue dans le corps de la boucle. Je trouve donc raisonnable que la norme autorise des exceptions lors de la comparaison de pointeurs de position après le dernier élément d'un tableau.
PVS-Studio Team NoteLors du développement de l'analyseur de code PVS-Studio, nous devons parfois faire face à des problèmes subtils afin de rendre les diagnostics plus précis ou de donner des consultations détaillées à nos clients. Cet article nous a paru intéressant, car il touche à des questions sur lesquelles nous ne nous sentons pas pleinement confiants. Par conséquent, nous avons demandé à l'auteur de publier sa traduction. Nous espérons que davantage de programmeurs C et C ++ apprendront à la connaître et comprendront que ce n'est pas si simple et que lorsque l'analyseur affiche soudainement un message étrange, vous ne devriez pas vous précipiter pour le considérer comme un faux positif :).L'article a d'abord été publié en anglais sur stefansf.de. Les traductions sont publiées avec la permission de l'auteur.