L'impression de pointeurs nuls avec% p est un comportement non défini?

93

Est-ce un comportement non défini pour imprimer des pointeurs nuls avec le %pspécificateur de conversion?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

La question s'applique au standard C, et non aux implémentations C.

Dror K.
la source
Je ne pense pas que quiconque (y compris le comité C) s'en soucie trop. C'est un problème assez artificiel, sans signification pratique (ou presque).
P__J__
c'est comme printf n'affiche que la valeur, et ne touche pas (dans le sens de la lecture ou de l'écriture de l'objet pointé) - ne peut pas être UB i le pointeur a une valeur valide pour son type (NULL est la valeur valide )
P__J__
3
@PeterJ disons que ce que vous dites est vrai (bien que la norme indique clairement le contraire), le seul fait que nous débattions de cela rend la question valide et correcte, car elle ressemble à la partie citée ci-dessous de la norme. il est très difficile de comprendre pour un développeur régulier ce qui se passe. Ce qui veut dire: la question ne mérite pas le vote négatif, car ce problème nécessite une clarification!
Peter Varo
2
@PeterJ c'est une histoire différente alors, merci pour la clarification :)
Peter Varo

Réponses:

93

C'est l'un de ces cas étranges où nous sommes soumis aux limitations de la langue anglaise et à la structure incohérente de la norme. Donc, au mieux, je peux faire un contre-argument convaincant, car il est impossible de le prouver :) 1


Le code de la question présente un comportement bien défini.

Comme [7.1.4] est la base de la question, commençons par là:

Chacune des instructions suivantes s'applique, sauf indication contraire explicite dans les descriptions détaillées qui suivent: Si un argument d'une fonction a une valeur non valide ( telle qu'une valeur en dehors du domaine de la fonction, ou un pointeur en dehors de l'espace d'adressage du programme, ou un pointeur nul , [... autres exemples ...] ) [...] le comportement n'est pas défini. [... autres déclarations ...]

C'est un langage maladroit. Une interprétation est que les éléments de la liste sont UB pour toutes les fonctions de la bibliothèque, à moins qu'ils ne soient remplacés par les descriptions individuelles. Mais la liste commence par "comme", indiquant que c'est illustratif et non exhaustif. Par exemple, il ne mentionne pas la terminaison nulle correcte des chaînes (critique pour le comportement de par exemple strcpy).

Il est donc clair que l'intention / la portée de 7.1.4 est simplement qu'une "valeur invalide" mène à UB ( sauf indication contraire ). Nous devons examiner la description de chaque fonction pour déterminer ce qui compte comme "valeur invalide".

Exemple 1 - strcpy

[7.21.2.3] dit seulement ceci:

La strcpyfonction copie la chaîne pointée par s2(y compris le caractère nul de fin) dans le tableau pointé par s1. Si la copie a lieu entre des objets qui se chevauchent, le comportement n'est pas défini.

Il ne fait aucune mention explicite des pointeurs nuls, mais il ne fait pas non plus mention des terminateurs nuls. Au lieu de cela, on déduit de "chaîne pointée par s2" que les seules valeurs valides sont des chaînes (c'est-à-dire des pointeurs vers des tableaux de caractères terminés par un nul).

En effet, ce modèle peut être vu à travers les descriptions individuelles. Quelques autres exemples:

  • [7.6.4.1 (fenv)] stocke l'environnement en virgule flottante actuel dans l' objet pointé parenvp

  • [7.12.6.4 (frexp)] stocke l'entier dans l' objet int pointé parexp

  • [7.19.5.1 (fclose)] le flux pointé parstream

Exemple 2 - printf

[7.19.6.1] dit ceci à propos de %p:

p- L'argument doit être un pointeur vers void. La valeur du pointeur est convertie en une séquence de caractères d'impression, d'une manière définie par l'implémentation.

Null est une valeur de pointeur valide, et cette section ne fait aucune mention explicite que null est un cas particulier, ni que le pointeur doit pointer sur un objet. C'est donc un comportement défini.


1. À moins qu'un auteur de normes ne se présente, ou à moins que nous puissions trouver quelque chose de similaire à un document de justification qui clarifie les choses.

Oliver Charlesworth
la source
Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Bhargav Rao
1
"pourtant il ne fait aucune mention des terminateurs nuls" est faible dans l'exemple 1 - strcpy car la spécification dit "copie la chaîne ". string est explicitement défini comme ayant un caractère nul .
chux - Réintégrer Monica le
1
@chux - C'est un peu mon point - il faut déduire ce qui est valide / invalide à partir du contexte, plutôt que de supposer que la liste en 7.1.4 est exhaustive. (Cependant, l'existence de cette partie de ma réponse avait un peu plus de sens dans le contexte des commentaires qui ont depuis été supprimés, arguant que strcpy était un contre-exemple.)
Oliver Charlesworth
1
Le nœud du problème est de savoir comment le lecteur interprétera comme . Cela signifie-t-il que certains exemples de valeurs invalides possibles sont ? Cela signifie-t-il que certains exemples sont toujours des valeurs invalides ? Pour mémoire, je suis d'accord avec la première interprétation.
ninjalj
1
@ninjalj - Oui, d'accord. C'est essentiellement ce que j'essaie de transmettre dans ma réponse ici, à savoir "ce sont des exemples de types de choses qui pourraient être des valeurs invalides". :)
Oliver Charlesworth
20

La réponse courte

Oui . L'impression de pointeurs nuls avec le %pspécificateur de conversion a un comportement non défini. Cela dit, je ne connais aucune implémentation conforme existante qui se conduirait mal.

La réponse s'applique à n'importe laquelle des normes C (C89 / C99 / C11).


La longue réponse

Le %pspécificateur de conversion attend un argument de type pointeur vers void, la conversion du pointeur en caractères imprimables est définie par l'implémentation. Il n'indique pas qu'un pointeur nul est attendu.

L'introduction aux fonctions de bibliothèque standard indique que les pointeurs nuls en tant qu'arguments aux fonctions (de bibliothèque standard) sont considérés comme des valeurs non valides, sauf indication contraire explicite.

C99 / C11 §7.1.4 p1

[...] Si un argument d'une fonction a une valeur non valide (comme [...] un pointeur nul, [...] le comportement n'est pas défini.

Exemples de fonctions (bibliothèque standard) qui attendent des pointeurs nuls comme arguments valides:

  • fflush() utilise un pointeur nul pour vider "tous les flux" (qui s'appliquent).
  • freopen() utilise un pointeur nul pour indiquer le fichier "actuellement associé" au flux.
  • snprintf() permet de passer un pointeur nul lorsque 'n' vaut zéro.
  • realloc() utilise un pointeur nul pour allouer un nouvel objet.
  • free() permet de passer un pointeur nul.
  • strtok() utilise un pointeur nul pour les appels suivants.

Si nous prenons le cas pour snprintf(), il est logique d'autoriser le passage d'un pointeur nul lorsque 'n' est égal à zéro, mais ce n'est pas le cas pour les autres fonctions (bibliothèque standard) qui autorisent un zéro similaire 'n'. Par exemple: memcpy(), memmove(), strncpy(), memset(), memcmp().

Ce n'est pas seulement spécifié dans l'introduction à la bibliothèque standard, mais aussi une fois encore dans l'introduction à ces fonctions:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Lorsqu'un argument déclaré comme size_tn spécifie la longueur du tableau pour une fonction, n peut avoir la valeur zéro lors d'un appel à cette fonction. Sauf indication contraire explicite dans la description d'une fonction particulière dans le présent paragraphe, les arguments de pointeur sur un tel appel doivent toujours avoir des valeurs valides comme décrit au 7.1.4.


Est-ce intentionnel?

Je ne sais pas si l'UB %pavec un pointeur nul est en fait intentionnel, mais puisque la norme indique explicitement que les pointeurs nulles sont considérés comme des valeurs non valides comme arguments des fonctions de bibliothèque standard, puis il spécifie explicitement les cas où un nul pointeur est un argument valide (snprintf, libre, etc.), puis il va et répète encore une fois l'exigence selon laquelle les arguments soient valides même dans zéro cas « n » ( memcpy, memmove, memset), alors je pense qu'il est raisonnable de supposer que la Le comité des normes C n'est pas trop préoccupé par le fait que de telles choses ne soient pas définies.

Dror K.
la source
Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Bhargav Rao
1
@JeroenMostert: Quelle est l'intention de cet argument? La citation donnée de 7.1.4 est plutôt claire, n'est-ce pas? Sur quoi peut-on argumenter "sauf indication contraire explicite" alors qu'il n'est pas indiqué autrement? Qu'y a-t-il à argumenter sur le fait que la bibliothèque de fonctions de chaîne (non liée) a un libellé similaire, de sorte que le libellé ne semble pas être accidentel? Je pense que cette réponse (bien que pas vraiment utile dans la pratique ) est aussi correcte que possible.
Damon
3
@Damon: Votre matériel mythique n'est pas mythique, il existe de nombreuses architectures où les valeurs qui ne représentent pas des adresses valides peuvent ne pas être chargées dans les registres d'adresses. Cependant, il est toujours nécessaire de passer des pointeurs nuls comme arguments de fonction pour fonctionner sur ces plates-formes en tant que mécanisme général. Le simple fait d'en mettre un sur la pile ne fera pas exploser les choses.
Jeroen Mostert
1
@anatolyg: sur les processeurs x86, les adresses comportent deux parties: un segment et un décalage. Sur le 8086, charger un registre de segment équivaut à charger n'importe quel autre, mais sur toutes les machines ultérieures, il récupère un descripteur de segment. Le chargement d'un descripteur non valide provoque une interruption. Beaucoup de code pour 80386 et processeurs plus tard, cependant, utilise un seul segment, et donc jamais charges des registres de segment du tout .
supercat du
1
Je pense que tout le monde conviendrait que l'impression d'un pointeur nul avec %pn'est pas censé être un comportement indéfini
MM
-1

Les auteurs de la norme C n'ont fait aucun effort pour énumérer de manière exhaustive toutes les exigences comportementales qu'une implémentation doit satisfaire pour convenir à un usage particulier. Au lieu de cela, ils s'attendaient à ce que les personnes qui rédigent des compilateurs exercent un certain sens commun, que la norme l'exige ou non.

La question de savoir si quelque chose invoque UB est rarement utile en soi. Les vraies questions d'importance sont:

  1. Quelqu'un qui essaie d'écrire un compilateur de qualité devrait-il le faire se comporter de manière prévisible? Pour le scénario décrit, la réponse est clairement oui.

  2. Les programmeurs devraient-ils être en droit de s'attendre à ce que des compilateurs de qualité pour tout ce qui ressemble à des plates-formes normales se comportent de manière prévisible? Dans le scénario décrit, je dirais que la réponse est oui.

  3. Certains compilateurs obtus pourraient-ils étirer l'interprétation du Standard afin de justifier de faire quelque chose de bizarre? J'espère que non, mais je ne l'exclurais pas.

  4. Les compilateurs de désinfection devraient-ils se plaindre du comportement? Cela dépendrait du niveau de paranoïa de leurs utilisateurs; un compilateur de désinfection ne devrait probablement pas par défaut se plaindre d'un tel comportement, mais peut-être fournir une option de configuration à faire au cas où les programmes seraient portés vers des compilateurs "intelligents" / stupides qui se comportent de façon étrange.

Si une interprétation raisonnable de la norme impliquerait qu'un comportement est défini, mais que certains rédacteurs de compilateurs étendent l'interprétation pour justifier de faire autrement, est-ce vraiment important ce que dit la norme?

supercat
la source
1. Il n'est pas rare que les programmeurs trouvent que les hypothèses faites par les optimiseurs modernes / agressifs sont en contradiction avec ce qu'ils considèrent comme "raisonnable" ou "de qualité". 2. Quand il s'agit d'ambiguïtés dans la spécification, il n'est pas rare que les implémenteurs soient en désaccord sur les libertés qu'ils peuvent assumer. 3. En ce qui concerne les membres du comité des normes C, même s'ils ne sont pas toujours d'accord sur ce qu'est l'interprétation «correcte», et encore moins sur ce qu'elle devrait être. Compte tenu de ce qui précède, à quelle interprétation raisonnable devrions-nous suivre?
Dror K.
6
Répondre à la question "ce morceau de code particulier invoque-t-il UB ou non" avec une dissertation sur ce que vous pensez de l'utilité d'UB ou sur la manière dont les compilateurs devraient se comporter est une mauvaise tentative de réponse, d'autant plus que vous pouvez copier-coller ceci comme une réponse à presque toutes les questions sur UB en particulier. En guise de réplique à votre floraison rhétorique: oui, ce que dit le Standard importe vraiment, peu importe ce que font certains rédacteurs de compilateurs ou ce que vous pensez d'eux pour le faire, car le Standard est ce à partir de quoi les programmeurs et les rédacteurs de compilateurs partent.
Jeroen Mostert
1
@JeroenMostert: La réponse à "X invoque-t-elle un comportement non défini" dépendra souvent de ce que l'on entend par question. Si un programme est considéré comme ayant un comportement indéfini si la norme n'impose aucune exigence sur le comportement d'une implémentation conforme, alors presque tous les programmes invoquent UB. Les auteurs de la norme permettent clairement aux implémentations de se comporter de manière arbitraire si un programme imbrique les appels de fonction trop profondément, à condition qu'une implémentation puisse traiter correctement au moins un texte source (éventuellement artificiel) qui exerce les limites de traduction dans le Stadard.
supercat
@supercat: très intéressant, mais le printf("%p", (void*) 0)comportement est-il indéfini ou pas, selon le Standard? Les appels de fonction profondément imbriqués sont aussi pertinents que le prix du thé en Chine. Et oui, UB est très courant dans les programmes du monde réel - qu'en est-il?
Jeroen Mostert
1
@JeroenMostert: Puisque le Standard permettrait à une implémentation obtuse de considérer presque n'importe quel programme comme ayant UB, ce qui devrait compter sera le comportement des implémentations non obtuses. Au cas où vous ne l'auriez pas remarqué, je n'ai pas simplement écrit un copier / coller sur UB, mais j'ai répondu à la question sur %pchaque signification possible de la question.
supercat