web-dev-qa-db-fra.com

C vérifie-t-il si un pointeur est hors limites sans qu'il soit déréférencé?

J'ai eu cette dispute avec des gens qui disaient que les pointeurs C hors limites provoquaient un comportement indéfini même s'ils n'étaient pas déréférencés. Exemple:

int a;
int *p = &a;
p = p - 1;

la troisième ligne provoquera un comportement indéfini même si p n'est jamais déréférencé (*p n'est jamais utilisé).

À mon avis, il semble illogique que C vérifie si un pointeur est hors de portée sans être utilisé (c'est comme si quelqu'un inspectait des personnes dans la rue pour voir si elles portaient des armes à feu au cas où elles entreraient chez lui. La meilleure chose à faire est d’inspecter les personnes au moment où elles vont entrer dans la maison). Je pense que si C vérifie cela, il y aura beaucoup de temps système.

De plus, si C vérifie vraiment les pointeurs hors bande, pourquoi cela ne causera pas d'UB:

int *p; // uninitialized thus pointing to a random adress

dans ce cas, pourquoi rien ne se produit même si la probabilité que p pointe vers une adresse OOB est élevée.

AJOUTER:

int a;
int *p = &a;
p = p - 1;

&a vaut 1000. Est-ce que la valeur de p après l'évaluation de la troisième ligne sera:

  • 996 Mais le comportement n'est toujours pas défini car p pourrait être déréférencé ailleurs et causer le vrai problème.
  • valeur indéfinie et c'est le comportement indéfini.

parce que je pense que "la troisième ligne était appelée à avoir un comportement indéfini" en premier lieu, c'était à cause de l'utilisation future potentielle de ce pointeur OOB (déréférencement) et les gens, au fil du temps, l'ont pris comme un comportement indéfini. Maintenant, la valeur de p sera-t-elle égale à 100% 996 et ce comportement toujours indéfini ou sa valeur sera-t-elle indéfinie?

40
ibrahim mahrir

C n'est pas vérifie si un pointeur est hors limites. Mais le matériel sous-jacent peut se comporter de manière étrange lorsqu’une adresse est calculée qui tombe en dehors des limites de l’objet, pointant juste après la fin d’un objet constituant une exception. La norme C décrit explicitement cela comme provoquant un comportement non défini.

Pour la plupart des environnements actuels, le code ci-dessus ne pose pas de problème, mais des situations similaires pourraient entraîner des erreurs de segmentation en mode protégé x86 16 bits, il y a environ 25 ans.

Dans le langage de la norme, une telle valeur pourrait être un valeur d'interception, qui ne peut pas être manipulé sans invoquer un comportement indéfini.

La section pertinente de la norme C11 est la suivante:

6.5.6 Opérateurs additifs

  1. Lorsqu'une expression de type entier est ajoutée ou soustraite à un pointeur, le résultat a le type de l'opérande de pointeur. Si l'opérande de pointeur pointe sur un élément d'un objet tableau et que le tableau est suffisamment grand, le résultat pointe sur un élément décalé par rapport à l'élément d'origine de sorte que la différence entre les indices des éléments du tableau résultant et original soit égale à l'expression entière. [...] Si l'opérande de pointeur et le résultat pointent tous deux sur des éléments du même objet de tableau, ou sur un élément situé après le dernier élément de l'objet de tableau, l'évaluation ne doit pas produire de dépassement de capacité; sinon, le comportement n'est pas défini. Si le résultat pointe un après le dernier élément de l'objet tableau, il ne doit pas être utilisé comme opérande d'un opérateur unaire * évalué.

Voici un exemple similaire de comportement non défini:

char *p;
char *q = p;

Le simple chargement de la valeur du pointeur non initialisé p invoque un comportement non défini, même s'il n'est jamais déréférencé.

EDIT: il est un point discutable d'essayer de discuter à ce sujet. La norme indique que le calcul d’une telle adresse appelle un comportement indéfini, donc il le fait. Le fait que certaines implémentations puissent simplement calculer une valeur et la stocker ou non est sans importance. Ne vous fiez à aucune hypothèse concernant un comportement indéfini: le compilateur pourrait tirer parti de sa nature intrinsèquement imprévisible pour effectuer des optimisations que vous ne pouvez pas imaginer.

Par exemple cette boucle:

for (int i = 1; i != 0; i++) {
    ...
}

peut compiler en une boucle infinie sans aucun test: i++ invoque un comportement indéfini si i est INT_MAX, l'analyse du compilateur est donc la suivante:

  • la valeur initiale de i est > 0.
  • pour toute valeur positive de i < INT_MAX, i++ est toujours > 0
  • pour i = INT_MAX, i++ invoque un comportement indéfini, nous pouvons donc supposer i > 0 car nous pouvons supposer tout ce que nous voulons.

Par conséquent, i est toujours > 0 et le code de test peut être supprimé.

67
chqrlie

En effet, le comportement d'un programme C n'est pas défini s'il tente de calculer une valeur par le biais d'une arithmétique de pointeur qui ne résulte pas en un pointeur vers un élément, ou après la fin du même élément de tableau. À partir de C11 6.5.6/8:

Si à la fois le pointeur L'opérande et le résultat pointent sur des éléments du même objet de tableau, ou l'un après le dernier élément de l'objet tableau, l'évaluation ne doit pas produire de dépassement de capacité; sinon, le le comportement est indéfini.

(Pour les besoins de cette description, l'adresse d'un objet de type T peut être traitée comme l'adresse du premier élément d'un tableau T[1].)

21
Kerrek SB

Pour clarifier, "comportement non défini" signifie que le résultat du code en question n'est pas défini dans les normes régissant le langage. Le résultat réel dépend de la manière dont le compilateur est implémenté et peut aller de rien à un crash complet.

Les normes ne spécifient pas qu’une vérification de plage des pointeurs doit avoir lieu. Mais en ce qui concerne votre exemple spécifique, voici ce qu’ils disent:

Lorsqu'une expression de type entier est ajoutée ou soustraite depuis un pointeur ... Si l'opérande du pointeur et le résultat pointent vers éléments du même objet de tableau, ou un après le dernier élément de la objet tableau, l'évaluation ne doit pas produire de débordement; autrement, le comportement est indéfini. Si le résultat pointe un après le dernier élément de l'objet tableau, il ne doit pas être utilisé comme opérande d'un opérateur unaire * évalué.

La citation ci-dessus provient de C99 §6.5.6 Para 8 (la version la plus récente que j'ai sous la main).

Notez que ce qui précède s’applique également aux pointeurs autres que les tableaux, car il est dit dans la clause précédente:

Pour les besoins de ces opérateurs, un pointeur sur un objet qui est pas un élément d'un tableau ne se comporte comme un pointeur sur le premier élément d'un tableau de longueur un avec le type de l'objet comme son type d'élément.

Ainsi, si vous effectuez une arithmétique de pointeur et que le résultat est compris entre des limites ou pointe vers un point situé au-delà de la fin de l'objet, vous obtiendrez un résultat valide, sinon vous obtiendrez un comportement non défini. Ce comportement peut être que vous vous retrouvez avec un pointeur égaré, mais cela peut être autre chose.

15
harmic

Oui, il s'agit d'un comportement non défini même si le pointeur n'est pas déréférencé.

C n'autorise que les pointeurs à pointer un seul élément après les limites du tableau .

7
Kornel

Certaines plates-formes traitent les pointeurs comme des entiers et traitent l'arithmétique de pointeur de la même manière que l'arithmétique d'entiers, mais avec certaines valeurs mises à l'échelle en plus ou en moins en fonction de la taille des objets. Sur de telles plateformes, cela définira effectivement un résultat "naturel" de toutes les opérations arithmétiques de pointeur, à l'exception de la soustraction de pointeurs dont la différence n'est pas un multiple de la taille du type de cible du pointeur.

D'autres plates-formes peuvent représenter des pointeurs d'une autre manière, et l'addition ou la soustraction de certaines combinaisons de pointeurs peut entraîner des résultats imprévisibles.

Les auteurs de la norme C ne voulaient pas faire preuve de favoritisme envers l'un ou l'autre type de plate-forme. Par conséquent, cela n'impose aucune exigence quant à ce qui pourrait arriver si les pointeurs étaient manipulés de manière à causer des problèmes sur certaines plates-formes. Avant la norme C, et quelques années plus tard, les programmeurs pouvaient raisonnablement s’attendre à ce que les implémentations à usage général pour les plates-formes qui traitent l’arithmétique de pointeur comme l’arithmétique de graduation d’entier traitent également l’arithmétique de pointeur de la même manière, mais les implémentations pour les plates-formes traitant l’arithmétique de pointeur différemment. serait probablement traiter différemment eux-mêmes.

Au cours de la dernière décennie environ, toutefois, à la recherche d'une "optimisation", les rédacteurs de compilateurs ont décidé de jeter le Principe de moindre étonnement par la fenêtre. Même dans les cas où un programmeur saurait l'effet que certaines opérations de pointeur auraient sur les représentations naturelles du pointeur d'une plate-forme, rien ne garantit que les compilateurs vont générer du code qui se comporte comme les représentations naturelles du pointeur. Le fait que la norme indique que le comportement n'est pas défini est interprété comme une invitation des compilateurs à imposer des "optimisations" obligeant les programmeurs à écrire du code plus lent et plus encombrant qu'il ne l'aurait dû pour des implémentations se comportant simplement de manière cohérente avec le document. comportements de l’environnement sous-jacent (l’un des trois traitements que les auteurs du C89 ont explicitement qualifié de banal).

Ainsi, à moins de savoir que l’on utilise un compilateur pour lequel aucune "optimisation" farfelue n’est activée, le fait qu’une étape intermédiaire dans une séquence de calculs de pointeurs invoque le comportement indéfini empêche tout raisonnement de le faire, peu importe la raison. à quel point le bon sens impliquerait-il qu'une implémentation de qualité pour une plate-forme particulière doit se comporter de manière particulière.

4
supercat

"Un comportement indéfini" signifie "tout peut arriver". Les valeurs communes de "n'importe quoi" sont "rien ne se passe mal" et "votre code plante". Les autres valeurs communes de "n'importe quoi" sont "les mauvaises choses arrivent lorsque vous activez l'optimisation", ou "les mauvaises choses arrivent lorsque vous n'exécutez pas le code en développement mais qu'un client l'exécute", et d'autres valeurs encore sont "votre code fait quelque chose d'inattendu "et" votre code fait quelque chose qu'il ne devrait pas pouvoir faire ". 

Donc, si vous dites "cela semble illogique que C vérifie si un pointeur est hors d'usage sans utiliser le pointeur", vous vous trouvez dans un territoire très, très, très dangereux. Prenez ce code:

int a = 0;
int b [2] = { 1, 2 };
int* p = &a; p - 1;
printf ("%d\n", *p);

Le compilateur peut supposer qu'il n'y a pas de comportement indéfini. p-1 a été évalué. Le compilateur conclut (légalement) que p = & a [1], p = & b [1] ou p = & b [2], car dans tous les autres cas, il existe un comportement indéfini lors de l'évaluation de p ou de p-1. Le compilateur suppose alors que * p n'est pas un comportement indéfini. Il conclut donc (légalement) que p = & b [1] et affiche la valeur 2. Vous ne vous attendiez pas à cela, n'est-ce pas? 

C'est légal, et cela arrive . La leçon est donc la suivante: n’invoquez PAS un comportement indéfini. 

4
gnasher729

La partie de la question relative au comportement indéfini est très claire, la réponse est "Eh bien oui, c’est certainement un comportement indéfini".

Je vais interpréter le libellé "Est-ce que C vérifie ..." comme suit:

  1. Le compilateur C vérifie-t-il ...?
  2. Mon programme compilé vérifie-t-il ...?

(C lui-même est une spécification de langage, il ne vérifie ni ne fait quoi que ce soit)

La réponse à la première question est la suivante: oui, mais pas de manière fiable et pas comme vous le souhaitez. Les compilateurs modernes sont assez intelligents, parfois plus intelligents que vous le souhaiteriez. Dans certains cas, le compilateur pourra diagnostiquer votre utilisation illégitime de pointeurs. Puisque per definition invoque un comportement indéfini et que le langage n'exige donc plus que le compilateur fasse quoi que ce soit en particulier, le compilateur optimisera souvent de manière imprévisible. Cela peut entraîner un code très différent de celui que vous aviez initialement prévu. Ne soyez pas surpris si une portée entière ou même la fonction complète devient complètement dévastée. Cela est vrai pour de nombreuses "optimisations surprises" indésirables liées à un comportement indéfini.
Lecture obligatoire: Ce que tout programmeur C devrait savoir sur le comportement non défini .

La réponse à la deuxième question est la suivante: Non, sauf si vous utilisez un compilateur qui prend en charge les vérifications de limites et si vous compilez avec les vérifications de limites d'exécution activées, ce qui implique une surcharge d'exécution non triviale.
En pratique, cela signifie que si votre programme "survit" au compilateur en optimisant un comportement indéfini, il fera obstinément ce que vous lui avez dit de faire, avec des résultats imprévisibles - généralement des valeurs erronées lues ou votre programme. provoquant une erreur de segmentation.

3
Damon

Mais quel est le comportement indéfini du est? Cela signifie simplement que personne ne veut dire ce qui va arriver.

Je suis un vieux chien de l'ordinateur central depuis des années et j'aime bien la phrase d'IBM pour la même chose: les résultats sont imprévisibles.

BTW: J'aime l'idée de PAS vérifier les limites du tableau. Par exemple, si j'ai un pointeur sur une chaîne et que je veux voir ce qui se trouve juste avant l'octet pointé, je peux utiliser:

pointer[-1]

le regarder.

1
Jennifer