Pourquoi printf («% f», 0); donner un comportement indéfini?

87

La déclaration

printf("%f\n",0.0f);

imprime 0.

Cependant, la déclaration

printf("%f\n",0);

imprime des valeurs aléatoires.

Je me rends compte que j'expose une sorte de comportement indéfini, mais je ne peux pas comprendre pourquoi spécifiquement.

Une valeur en virgule flottante dans laquelle tous les bits sont 0 est toujours valide floatavec une valeur de 0.
floatet intont la même taille sur ma machine (si cela est même pertinent).

Pourquoi l'utilisation d'un littéral entier au lieu d'un littéral à virgule flottante printfprovoque-t-il ce comportement?

PS le même comportement peut être vu si j'utilise

int i = 0;
printf("%f\n", i);
Trevor Hickey
la source
37
printfattend un double, et vous lui donnez un int. floatet intpeut être de la même taille sur votre machine, mais 0.0fest en fait converti en un doublelorsqu'il est poussé dans une liste d'arguments variadiques (et il s'y printfattend). En bref, vous ne remplissez pas votre part du marché en printffonction des spécificateurs que vous utilisez et des arguments que vous fournissez.
WhozCraig
22
Les fonctions Varargs ne convertissent pas automatiquement les arguments de fonction en type du paramètre correspondant, car elles ne le peuvent pas. Les informations nécessaires ne sont pas disponibles pour le compilateur, contrairement aux fonctions non varargs avec un prototype.
EOF du
3
Oooh ... "variadics." Je viens d'apprendre un nouveau mot ...
Mike Robinson
2
Copie possible: Printf peut-il entraîner un comportement indéfini?
Khalil Khalaf le
3
La prochaine chose à essayer est de passer un (uint64_t)0au lieu de 0et de voir si vous obtenez toujours un comportement aléatoire (en supposant doubleque vous uint64_tayez la même taille et l'alignement). Il y a de fortes chances que la sortie soit toujours aléatoire sur certaines plates-formes (par exemple x86_64) en raison de différents types passés dans différents registres.
Ian Abbott le

Réponses:

121

Le "%f"format nécessite un argument de type double. Vous lui donnez un argument de type int. C'est pourquoi le comportement n'est pas défini.

La norme ne garantit pas que tous les bits à zéro sont une représentation valide de 0.0(bien que ce soit souvent le cas), ou de n'importe quelle doublevaleur, ou que intet doublesoient de la même taille (rappelez-vous que ce n'est doublepas le cas float), ou, même s'ils sont identiques size, qu'ils sont passés comme arguments à une fonction variadique de la même manière.

Cela peut arriver à "fonctionner" sur votre système. C'est le pire symptôme possible d'un comportement indéfini, car il est difficile de diagnostiquer l'erreur.

N1570 7.21.6.1 paragraphe 9:

... Si un argument n'est pas du type correct pour la spécification de conversion correspondante, le comportement n'est pas défini.

Les arguments de type floatsont promus double, c'est pourquoi cela printf("%f\n",0.0f)fonctionne. Arguments de types entiers plus étroits que ceux intpromus vers intou vers unsigned int. Ces règles de promotion (spécifiées par N1570 6.5.2.2 paragraphe 6) n'aident pas dans le cas de printf("%f\n", 0).

Notez que si vous passez une constante 0à une fonction non variadique qui attend un doubleargument, le comportement est bien défini, en supposant que le prototype de la fonction est visible. Par exemple, sqrt(0)(après #include <math.h>) convertit implicitement l'argument 0de inten double- car le compilateur peut voir à partir de la déclaration sqrtqu'il attend un doubleargument. Il n'a pas de telles informations pour printf. Les fonctions variadiques telles que printfsont spéciales et nécessitent plus de soin lors de l'écriture des appels.

Keith Thompson
la source
13
Quelques excellents points essentiels ici. Premièrement, ce n'est doublepas le floatcas, l'hypothèse de largeur de l'OP peut ne pas (probablement pas) tenir. Deuxièmement, l'hypothèse selon laquelle le zéro entier et le zéro à virgule flottante ont le même motif de bits ne tient pas non plus. Good work
Lightness Races in Orbit
2
@LucasTrzesniewski: Ok, mais je ne vois pas comment ma réponse soulève la question. J'ai déclaré que c'était floatpromu doublesans expliquer pourquoi, mais ce n'était pas le point principal.
Keith Thompson
2
@ robertbristow-johnson: Les compilateurs n'ont pas besoin d'avoir des hooks spéciaux pour printf, bien que gcc, par exemple, en ait certains pour pouvoir diagnostiquer les erreurs ( si la chaîne de format est un littéral). Le compilateur peut voir la déclaration de printffrom <stdio.h>, qui lui indique que le premier paramètre est a const char*et les autres sont indiqués par , .... Non, %fest pour double(et floatest promu double) et %lfest pour long double. Le standard C ne dit rien sur une pile. Il spécifie le comportement de printfuniquement lorsqu'il est appelé correctement.
Keith Thompson
2
@ robertbristow-johnson: Dans le passé, "lint" effectuait souvent une partie des vérifications supplémentaires que gcc effectue maintenant. Un floatpassé à printfest promu à double; il n'y a rien de magique là-dedans, c'est juste une règle de langage pour appeler des fonctions variadiques. printflui-même sait via la chaîne de format ce que l'appelant a prétendu lui transmettre; si cette affirmation est incorrecte, le comportement n'est pas défini.
Keith Thompson
2
Petite correction: le lmodificateur de longueur « n'a aucun effet sur un public a, A, e, E, f, F, gou Gspécificateur de conversion », le modificateur de longueur pour une long doubleconversion est L. (@ robertbristow-johnson pourrait également être intéressé)
Daniel Fischer
58

Tout d'abord, comme évoqué dans plusieurs autres réponses mais pas, à mon avis, assez clairement expliqué: cela fonctionne pour fournir un entier dans la plupart des contextes où une fonction de bibliothèque prend un argument doubleou float. Le compilateur insérera automatiquement une conversion. Par exemple, sqrt(0)est bien défini et se comportera exactement comme sqrt((double)0), et il en va de même pour toute autre expression de type entier utilisée ici.

printfest différent. C'est différent car il prend un nombre variable d'arguments. Son prototype de fonction est

extern int printf(const char *fmt, ...);

Par conséquent, lorsque vous écrivez

printf(message, 0);

le compilateur n'a aucune information sur le type printf attendu de ce second argument. Il n'a que le type de l'expression d'argument, qui est int, pour passer. Par conséquent, contrairement à la plupart des fonctions de bibliothèque, c'est à vous, le programmeur, de vous assurer que la liste d'arguments correspond aux attentes de la chaîne de format.

(Les compilateurs modernes peuvent examiner une chaîne de format et vous dire que vous avez une incompatibilité de type, mais ils ne vont pas commencer à insérer des conversions pour accomplir ce que vous vouliez dire, car mieux votre code devrait se casser maintenant, quand vous le remarquerez , que des années plus tard lors de la reconstruction avec un compilateur moins utile.)

Maintenant, l'autre moitié de la question était: étant donné que (int) 0 et (float) 0.0 sont, sur la plupart des systèmes modernes, tous deux représentés comme 32 bits qui sont tous à zéro, pourquoi ne fonctionne-t-il pas de toute façon, par accident? La norme C dit simplement "cela n'est pas nécessaire pour fonctionner, vous êtes seul", mais laissez-moi vous expliquer les deux raisons les plus courantes pour lesquelles cela ne fonctionnerait pas; cela vous aidera probablement à comprendre pourquoi ce n'est pas nécessaire.

Tout d' abord, pour des raisons historiques, lorsque vous passez une floatpar une liste d'arguments variable , il se promu à doublequi, sur la plupart des systèmes modernes, est 64 bits. Donc printf("%f", 0)passe seulement 32 bits de zéro à un appelé qui en attend 64.

La deuxième raison, tout aussi significative, est que les arguments de fonction à virgule flottante peuvent être passés à un endroit différent des arguments entiers. Par exemple, la plupart des processeurs ont des fichiers de registre séparés pour les entiers et les valeurs à virgule flottante, il se peut donc que les arguments 0 à 4 soient placés dans les registres r0 à r4 s'ils sont des entiers, mais f0 à f4 s'ils sont à virgule flottante. printf("%f", 0)Cherche donc dans le registre f1 ce zéro, mais il n'y est pas du tout.

zwol
la source
1
Existe-t-il des architectures qui utilisent des registres pour des fonctions variadiques, même parmi celles qui les utilisent pour des fonctions normales? Je pensais que c'était la raison pour laquelle les fonctions variadiques doivent être correctement déclarées même si d'autres fonctions [sauf celles avec des arguments float / short / char] peuvent être déclarées avec ().
Random832 du
3
@ Random832 De nos jours, la seule différence entre la convention d'appel d'une fonction variadique et normale est qu'il peut y avoir des données supplémentaires fournies à une variable variadique, comme le décompte du nombre réel d'arguments fournis. Sinon, tout va exactement au même endroit que pour une fonction normale. Voir par exemple la section 3.2 de x86-64.org/documentation/abi.pdf , où le seul traitement spécial pour les variadiques est un indice transmis AL. (Oui, cela signifie que la mise en œuvre de va_argest beaucoup plus compliquée qu'elle ne l'était auparavant.)
zwol
@ Random832: J'ai toujours pensé que la raison était que sur certaines architectures, des fonctions avec un nombre et un type d'arguments connus pouvaient être implémentées plus efficacement, en utilisant des instructions spéciales.
celtschk
@celtschk Vous pensez peut-être aux "fenêtres de registre" sur SPARC et IA64, censées accélérer le cas courant des appels de fonction avec un petit nombre d'arguments (hélas, en pratique, elles font exactement le contraire). Ils ne nécessitent pas que le compilateur traite spécialement les appels de fonctions variadiques, car le nombre d'arguments à un site d'appel est toujours une constante de compilation, que l'appelé soit ou non variadique.
zwol
@zwol: Non, je pensais à l' ret ninstruction du 8086, où se ntrouvait un entier codé en dur, qui n'était donc pas applicable pour les fonctions variadiques. Cependant, je ne sais pas si un compilateur C en a réellement profité (les compilateurs non-C l'ont certainement fait).
celtschk
13

Normalement, lorsque vous appelez une fonction qui attend a double, mais que vous fournissez un int, le compilateur se convertit automatiquement en a doublepour vous. Cela ne se produit pas printf, car les types d'arguments ne sont pas spécifiés dans le prototype de fonction - le compilateur ne sait pas qu'une conversion doit être appliquée.

Mark Ransom
la source
4
En outre, printf() en particulier est conçu pour que ses arguments puissent être de n'importe quel type. Vous devez savoir quel type est attendu par chaque élément de la chaîne de format et vous devez le fournir correctement.
Mike Robinson
@MikeRobinson: Eh bien, tout type C primitif. Ce qui est un très, très petit sous-ensemble de tous les types possibles.
MSalters
13

Pourquoi l'utilisation d'un littéral entier au lieu d'un littéral float provoque-t-il ce comportement?

Parce que printf()n'a pas de paramètres typés en plus const char* formatstringdu premier. Il utilise une ellipse de style c ( ...) pour tout le reste.

Il décide simplement comment interpréter les valeurs qui y sont passées en fonction des types de mise en forme donnés dans la chaîne de format.

Vous auriez le même genre de comportement indéfini que lorsque vous essayez

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB
πάντα ῥεῖ
la source
3
Certaines implémentations particulières de printfpeuvent fonctionner de cette façon (sauf que les éléments transmis sont des valeurs et non des adresses). Le standard C ne spécifie pas comment printf et les autres fonctions variadiques fonctionnent, il spécifie simplement leur comportement. En particulier, il n'y a aucune mention des cadres de pile.
Keith Thompson
Un petit problème: printfa un paramètre typé, la chaîne de format, qui est de type const char*. BTW, la question est étiquetée à la fois C et C ++, et C est vraiment plus pertinent; Je ne l'aurais probablement pas utilisé reinterpret_castcomme exemple.
Keith Thompson
Juste une observation intéressante: même comportement indéfini, et très probablement dû à un mécanisme identique, mais avec une petite différence de détail: en passant un int comme dans la question, l'UB se produit dans printf lorsque vous essayez d'interpréter l'int comme double - dans votre exemple , il arrive déjà à l' extérieur lors du déréférencement pf ...
Aconcagua
@Aconcagua Ajout d'une clarification.
πάντα ῥεῖ
Cet exemple de code est UB pour la violation stricte d'aliasing, un problème entièrement différent de ce que la question pose. Par exemple, vous ignorez complètement la possibilité que les flottants soient passés dans différents registres à des entiers.
MM
12

L'utilisation d'un printf()spécificateur "%f"et d'un type (int) 0incorrects conduit à un comportement indéfini.

Si une spécification de conversion n'est pas valide, le comportement n'est pas défini. C11dr §7.21.6.1 9

Les causes candidates de l'UB.

  1. C'est UB par spécification et la compilation est dérisoire - dit nuf.

  2. doubleet intsont de tailles différentes.

  3. doubleet intpeuvent transmettre leurs valeurs en utilisant différentes piles (pile générale ou FPU .)

  4. A double 0.0 peut ne pas être défini par un modèle de bit entièrement nul. (rare)

chux - Réintégrer Monica
la source
10

C'est l'une de ces excellentes opportunités d'apprendre de vos avertissements de compilateur.

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function main’:
fnord.c:8:2: warning: format ‘%f expects argument of type double’, but argument 2 has type int [-Wformat=]
  printf("%f\n",0);
  ^

ou

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

Donc, printfproduit un comportement indéfini parce que vous lui passez un type d'argument incompatible.

wyrm
la source
9

Je ne sais pas ce qui est déroutant.

Votre chaîne de format attend un double; vous fournissez à la place un fichier int.

La question de savoir si les deux types ont la même largeur de bits n'est absolument pas pertinente, sauf que cela peut vous aider à éviter d'obtenir des exceptions de violation de mémoire matérielle à partir d'un code cassé comme celui-ci.

Courses de légèreté en orbite
la source
3
@Voo: Ce modificateur de chaîne de format est malheureusement nommé, mais je ne vois toujours pas pourquoi vous penseriez qu'un intserait acceptable ici.
Courses de légèreté en orbite le
1
@Voo: "(qui serait également considéré comme un motif flottant valide)" Pourquoi un motif serait-il intconsidéré comme un motif flottant valide? Le complément à deux et divers encodages en virgule flottante n'ont presque rien en commun.
Courses de légèreté en orbite le
2
C'est déroutant car, pour la plupart des fonctions de bibliothèque, fournir le littéral entier 0à un argument tapé doublefera la bonne chose. Il n'est pas évident pour un débutant que le compilateur ne fasse pas la même conversion pour les printfemplacements d'argument adressés par %[efg].
zwol du
1
@Voo: Si vous souhaitez savoir à quel point cela peut mal tourner, considérez que sur x86-64 SysV ABI, les arguments à virgule flottante sont passés dans un jeu de registres différent des arguments entiers.
EOF du
1
@LightnessRacesinOrbit Je pense qu'il est toujours approprié de discuter de la raison pour laquelle quelque chose est UB, ce qui implique généralement de parler de la latitude de mise en œuvre autorisée et de ce qui se passe réellement dans les cas courants.
zwol le
4

"%f\n"garantit un résultat prévisible uniquement lorsque le deuxième printf()paramètre est de type double. Ensuite, un argument supplémentaire des fonctions variadiques fait l'objet d'une promotion d'argument par défaut. Les arguments entiers relèvent de la promotion d'entiers, ce qui ne donne jamais de valeurs typées à virgule flottante. Et les floatparamètres sont promus en double.

Pour couronner le tout: standard permet au deuxième argument d'être ou floatou doubleet rien d'autre.

Sergio
la source
4

Pourquoi il est formellement UB a maintenant été discuté dans plusieurs réponses.

La raison pour laquelle vous obtenez spécifiquement ce comportement dépend de la plate-forme, mais est probablement la suivante:

  • printfattend ses arguments selon la propagation vararg standard. Cela signifie qu'un floatsera un doubleet que tout ce qui est plus petit qu'un intsera un int.
  • Vous passez un intoù la fonction attend un double. Votre intest probablement 32 bits, votre double64 bits. Cela signifie que les quatre octets de pile commençant à l'endroit où l'argument est censé se trouver sont 0, mais les quatre octets suivants ont un contenu arbitraire. C'est ce qui est utilisé pour construire la valeur qui est affichée.
glglgl
la source
0

La cause principale de ce problème de «valeur indéterminée» réside dans le cast du pointeur sur la intvaleur transmise à la printfsection des paramètres variables vers un pointeur sur les doubletypes que la va_argmacro exécute.

Cela provoque une référence à une zone de mémoire qui n'a pas été complètement initialisée avec la valeur transmise en tant que paramètre au printf, car la doubletaille de la zone de mémoire tampon est supérieure à la inttaille.

Par conséquent, lorsque ce pointeur est déréférencé, il est renvoyé une valeur indéterminée, ou mieux une "valeur" qui contient en partie la valeur passée en paramètre à printf, et pour la partie restante pourrait provenir d'une autre zone de tampon de pile ou même d'une zone de code ( soulevant une exception de faute mémoire), un véritable buffer overflow .


Il peut prendre en compte ces parties spécifiques des implémentations de code spécifiques de "printf" et "va_arg" ...

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


l'implémentation réelle dans vprintf (compte tenu de gnu impl.) de la gestion de cas de code de paramètres à double valeur est:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



références

  1. projet gnu implémentation glibc de "printf" (vprintf))
  2. exemple de code de spécification de printf
  3. exemple de code de spécification de va_arg
Ciro Corvino
la source