Ceci est un exemple pour illustrer ma question qui implique un code beaucoup plus compliqué que je ne peux pas publier ici.
#include <stdio.h>
int main()
{
int a = 0;
for (int i = 0; i < 3; i++)
{
printf("Hello\n");
a = a + 1000000000;
}
}
Ce programme contient un comportement indéfini sur ma plateforme car a
il débordera sur la 3ème boucle.
Est-ce que cela fait que tout le programme a un comportement indéfini, ou seulement après le débordement ? Pourrait éventuellement travailler sur le compilateur qui a
va déborder de sorte qu'il peut déclarer la boucle entière non défini et pas la peine de courir les printfs même si elles se produisent avant tout le trop - plein?
(Tagged C et C ++ même s'ils sont différents car je serais intéressé par des réponses pour les deux langues si elles sont différentes.)
a
a
Réponses:
Si vous êtes intéressé par une réponse purement théorique, la norme C ++ autorise un comportement indéfini à «voyager dans le temps»:
En tant que tel, si votre programme contient un comportement non défini, le comportement de l' ensemble de votre programme n'est pas défini.
la source
sneeze()
fonction elle-même n'est pas définie sur tout élément de la classeDemon
(dont la variété nasale est une sous-classe), ce qui rend le tout circulaire de toute façon.printf
ne retourne pas, mais s'ilprintf
va revenir, alors le comportement non défini peut causer des problèmes avant d'printf
être appelé. Par conséquent, voyage dans le temps.printf("Hello\n");
puis la ligne suivante compile commeundoPrintf(); launchNuclearMissiles();
Tout d'abord, permettez-moi de corriger le titre de cette question:
Le comportement indéfini n'est pas (spécifiquement) du domaine de l'exécution.
Le comportement indéfini affecte toutes les étapes: compilation, liaison, chargement et exécution.
Quelques exemples pour consolider cela, gardez à l'esprit qu'aucune section n'est exhaustive:
LD_PRELOAD
astuces sous UnixC'est ce qui fait si peur à propos du comportement indéfini: il est quasiment impossible de prédire, à l'avance, quel comportement exact se produira, et cette prédiction doit être revue à chaque mise à jour de la chaîne d'outils, du système d'exploitation sous-jacent, ...
Je recommande de regarder cette vidéo de Michael Spencer (développeur LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .
la source
argc
le nombre de boucles, le casargc=1
ne produit pas d'UB et le compilateur serait obligé de gérer cela.i
ne peut pas être incrémenté plus deN
fois et donc que sa valeur est limitée.f(good);
fait quelque chose X etf(bad);
invoque un comportement indéfini, alors un programme qui appelle simplementf(good);
est garanti de faire X, mais ilf(good); f(bad);
n'est pas garanti de faire X.if(foo) f(good); else f(bad);
, un compilateur intelligent jettera la comparaison et produira et un inconditionnelfoo(good)
.Une optimisation agressive compilateur C ou de C ciblant un 16 bits
int
va savoir que le comportement sur l' ajout d'1000000000
unint
type est indéfini .Il est permis par l'une ou l'autre des normes de faire tout ce qu'il veut, ce qui pourrait inclure la suppression de tout le programme, en partant
int main(){}
.Mais qu'en est-il des plus grands
int
? Je ne connais pas encore de compilateur qui fasse cela (et je ne suis en aucun cas un expert en conception de compilateurs C et C ++), mais j'imagine que parfois un compilateur ciblant un 32 bitsint
ou plus comprendra que la boucle est infini (i
ne change pas) et donca
finira par déborder. Donc, encore une fois, il peut optimiser la sortie versint main(){}
. Le point que j'essaie de faire ici est que, à mesure que les optimisations du compilateur deviennent progressivement plus agressives, de plus en plus de constructions de comportement indéfinies se manifestent de manière inattendue.Le fait que votre boucle soit infinie n'est pas en soi indéfini puisque vous écrivez sur la sortie standard dans le corps de la boucle.
la source
int
est 16 bits, l'ajout aura lieu danslong
(parce que l'opérande littéral a le typelong
) où il est bien défini, puis sera converti par une conversion définie par l'implémentation enint
.printf
est défini par la norme pour toujours retournerTechniquement, sous la norme C ++, si un programme contient un comportement non défini, le comportement de l'ensemble du programme, même au moment de la compilation (avant même que le programme ne soit exécuté), n'est pas défini.
En pratique, comme le compilateur peut supposer (dans le cadre d'une optimisation) que le débordement ne se produira pas, au moins le comportement du programme à la troisième itération de la boucle (en supposant une machine 32 bits) sera indéfini, bien qu'il Il est probable que vous obtiendrez des résultats corrects avant la troisième itération. Cependant, comme le comportement de l'ensemble du programme est techniquement indéfini, rien n'empêche le programme de générer une sortie complètement incorrecte (y compris aucune sortie), de planter à l'exécution à tout moment pendant l'exécution, ou même de ne pas compiler complètement (car le comportement non défini s'étend à temps de compilation).
Un comportement non défini offre au compilateur plus de marge d'optimisation car il élimine certaines hypothèses sur ce que le code doit faire. Ce faisant, les programmes qui reposent sur des hypothèses impliquant un comportement non défini ne sont pas garantis de fonctionner comme prévu. En tant que tel, vous ne devez pas vous fier à un comportement particulier considéré comme non défini par la norme C ++.
la source
if(false) {}
périmètre? Cela empoisonne-t-il tout le programme, du fait que le compilateur suppose que toutes les branches contiennent des parties logiques bien définies et donc fonctionnant sur de fausses hypothèses?Pour comprendre pourquoi un comportement non défini peut `` voyager dans le temps '' comme le dit correctement @TartanLlama , jetons un coup d'œil à la règle du `` comme si '':
Avec cela, nous pourrions voir le programme comme une «boîte noire» avec une entrée et une sortie. L'entrée peut être une entrée utilisateur, des fichiers et bien d'autres choses. Le résultat est le «comportement observable» mentionné dans la norme.
La norme définit uniquement un mappage entre l'entrée et la sortie, rien d'autre. Il le fait en décrivant un «exemple de boîte noire», mais dit explicitement que toute autre boîte noire avec le même mappage est également valide. Cela signifie que le contenu de la boîte noire n'est pas pertinent.
Dans cet esprit, il ne serait pas logique de dire qu'un comportement indéfini se produit à un certain moment. Dans l' échantillon mise en œuvre de la boîte noire, on pourrait dire où et quand il arrive, mais le réel boîte noire pourrait être quelque chose de complètement différent, nous ne pouvons donc plus dire où et quand cela se produit. Théoriquement, un compilateur pourrait par exemple décider d'énumérer toutes les entrées possibles, et pré-calculer les sorties résultantes. Ensuite, le comportement non défini se serait produit lors de la compilation.
Un comportement indéfini est l'inexistence d'un mappage entre l'entrée et la sortie. Un programme peut avoir un comportement non défini pour certaines entrées, mais un comportement défini pour d'autres. Ensuite, le mappage entre l'entrée et la sortie est tout simplement incomplet; il y a une entrée pour laquelle aucun mappage à la sortie n'existe.
Le programme de la question a un comportement indéfini pour toute entrée, le mappage est donc vide.
la source
En supposant qu'il s'agisse de
int
32 bits, un comportement indéfini se produit à la troisième itération. Ainsi, si, par exemple, la boucle n'était accessible que conditionnellement, ou pouvait être interrompue conditionnellement avant la troisième itération, il n'y aurait pas de comportement indéfini à moins que la troisième itération ne soit réellement atteinte. Cependant, en cas de comportement indéfini, toute la sortie du programme est indéfinie, y compris la sortie qui est "dans le passé" par rapport à l'invocation d'un comportement non défini. Par exemple, dans votre cas, cela signifie qu'il n'y a aucune garantie de voir 3 messages "Hello" dans la sortie.la source
La réponse de TartanLlama est correcte. Le comportement non défini peut se produire à tout moment, même pendant la compilation. Cela peut sembler absurde, mais c'est une fonctionnalité clé pour permettre aux compilateurs de faire ce qu'ils doivent faire. Ce n'est pas toujours facile d'être un compilateur. Vous devez faire exactement ce que dit la spécification, à chaque fois. Cependant, il peut parfois être monstrueusement difficile de prouver qu'un comportement particulier se produit. Si vous vous souvenez du problème d'arrêt, il est plutôt trivial de développer un logiciel pour lequel vous ne pouvez pas prouver s'il termine ou entre dans une boucle infinie lorsqu'il est alimenté par une entrée particulière.
Nous pourrions rendre les compilateurs pessimistes et compiler constamment de peur que la prochaine instruction ne soit l'un de ces problèmes comme des problèmes, mais ce n'est pas raisonnable. Au lieu de cela, nous donnons un laissez-passer au compilateur: sur ces sujets de «comportement indéfini», ils sont libérés de toute responsabilité. Un comportement indéfini comprend tous les comportements qui sont si subtilement néfastes que nous avons du mal à les séparer des problèmes d'arrêt vraiment méchants et néfastes, etc.
Il y a un exemple que j'aime publier, même si j'avoue avoir perdu la source, je dois donc paraphraser. C'était à partir d'une version particulière de MySQL. Dans MySQL, ils avaient un tampon circulaire qui était rempli de données fournies par l'utilisateur. Bien sûr, ils voulaient s'assurer que les données ne débordaient pas de la mémoire tampon, ils avaient donc une vérification:
if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }
Cela semble assez sain d'esprit. Cependant, que se passe-t-il si numberOfNewChars est vraiment gros et déborde? Ensuite, il s'enroule et devient un pointeur plus petit que
endOfBufferPtr
, de sorte que la logique de débordement ne sera jamais appelée. Alors ils ont ajouté un deuxième chèque, avant celui-là:if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }
Il semble que vous vous êtes occupé de l'erreur de dépassement de la mémoire tampon, non? Cependant, un bogue a été soumis indiquant que ce tampon débordait sur une version particulière de Debian! Une enquête minutieuse a montré que cette version de Debian était la première à utiliser une version particulièrement avant-gardiste de gcc. Sur cette version de gcc, le compilateur a reconnu que currentPtr + numberOfNewChars peut ne jamais être un pointeur plus petit que currentPtr car le débordement des pointeurs est un comportement indéfini! C'était suffisant pour que gcc optimise l'intégralité de la vérification, et tout à coup, vous n'étiez pas protégé contre les débordements de tampon, même si vous avez écrit le code pour le vérifier!
C'était un comportement de spécification. Tout était légal (bien que d'après ce que j'ai entendu, gcc a annulé ce changement dans la prochaine version). Ce n'est pas ce que je considérerais comme un comportement intuitif, mais si vous étirez un peu votre imagination, il est facile de voir comment une légère variante de cette situation pourrait devenir un problème d'arrêt pour le compilateur. Pour cette raison, les rédacteurs de spécifications l'ont fait "Undefined Behavior" et ont déclaré que le compilateur pouvait faire absolument tout ce qu'il voulait.
la source
if(numberOfNewChars > endOfBufferPtr - currentPtr)
, à condition que numberOfNewChars ne puisse jamais être négatif et que currentPtr pointe toujours vers quelque part dans la mémoire tampon, vous n'avez même pas besoin de la vérification ridicule de "wraparound". (Je ne pense pas que le code que vous avez fourni ait le moindre espoir de fonctionner dans un tampon circulaire - vous avez omis tout ce qui est nécessaire pour cela dans la paraphrase, alorsAu-delà des réponses théoriques, une observation pratique serait que pendant longtemps les compilateurs ont appliqué diverses transformations sur des boucles pour réduire la quantité de travail effectué en leur sein. Par exemple, étant donné:
for (int i=0; i<n; i++) foo[i] = i*scale;
un compilateur pourrait transformer cela en:
int temp = 0; for (int i=0; i<n; i++) { foo[i] = temp; temp+=scale; }
Économisant ainsi une multiplication à chaque itération de boucle. Une forme supplémentaire d'optimisation, que les compilateurs ont adaptée avec divers degrés d'agressivité, transformerait cela en:
if (n > 0) { int temp1 = n*scale; int *temp2 = foo; do { temp1 -= scale; *temp2++ = temp1; } while(temp1); }
Même sur les machines avec une boucle silencieuse en cas de débordement, cela pourrait mal fonctionner s'il y avait un nombre inférieur à n qui, multiplié par l'échelle, donnerait 0. Cela pourrait également se transformer en une boucle sans fin si l'échelle était lue à partir de la mémoire plus d'une fois et quelque chose. a changé sa valeur de manière inattendue (dans tous les cas où "scale" pourrait changer à mi-boucle sans appeler UB, un compilateur ne serait pas autorisé à effectuer l'optimisation).
Alors que la plupart de ces optimisations n'auraient aucun problème dans les cas où deux types courts non signés sont multipliés pour donner une valeur comprise entre INT_MAX + 1 et UINT_MAX, gcc a certains cas où une telle multiplication dans une boucle peut provoquer une sortie anticipée de la boucle . Je n'ai pas remarqué de tels comportements provenant d'instructions de comparaison dans le code généré, mais c'est observable dans les cas où le compilateur utilise le débordement pour déduire qu'une boucle peut s'exécuter au plus 4 fois ou moins; il ne génère pas par défaut d'avertissements dans les cas où certaines entrées provoqueraient UB et d'autres pas, même si ses inférences font ignorer la limite supérieure de la boucle.
la source
Un comportement non défini est, par définition, une zone grise. Vous ne pouvez tout simplement pas prédire ce qu'il fera ou ne fera pas - c'est ce que signifie "comportement non défini" .
Depuis des temps immémoriaux, les programmeurs ont toujours essayé de récupérer les restes de définition d'une situation indéfinie. Ils ont un code qu'ils veulent vraiment utiliser, mais qui se révèle être non défini, de sorte qu'ils tentent d'argumenter: « Je sais que ça non défini, mais sûrement il sera, au pire, faire ceci ou cela, il ne le fera jamais que . " Et parfois, ces arguments sont plus ou moins justes - mais souvent, ils sont faux. Et à mesure que les compilateurs deviennent plus intelligents et plus intelligents (ou, diront certains, de plus en plus sournois), les limites de la question ne cessent de changer.
Donc, vraiment, si vous voulez écrire du code qui fonctionnera à coup sûr, et qui continuera à fonctionner pendant longtemps, il n'y a qu'un seul choix: éviter à tout prix le comportement indéfini. En vérité, si vous vous y mêlez, il reviendra vous hanter.
la source
Une chose que votre exemple ne prend pas en compte est l'optimisation.
a
est défini dans la boucle mais jamais utilisé, et un optimiseur pourrait résoudre ce problème. En tant que tel, il est légitime que l'optimiseur rejettea
complètement, et dans ce cas, tout comportement non défini disparaît comme la victime d'un boojum.Cependant, bien sûr, cela lui-même n'est pas défini, car l'optimisation n'est pas définie. :)
la source
Puisque cette question est à double balisage C et C ++, je vais essayer de répondre aux deux. C et C ++ adoptent ici des approches différentes.
En C, l'implémentation doit être capable de prouver que le comportement non défini sera invoqué afin de traiter l'ensemble du programme comme s'il avait un comportement non défini. Dans l'exemple des OP, il semblerait trivial pour le compilateur de prouver cela et c'est donc comme si tout le programme n'était pas défini.
Nous pouvons le voir à partir du rapport de défaut 109 qui, à son cœur, demande:
et la réponse a été:
En C ++, l'approche semble plus souple et suggérerait qu'un programme a un comportement indéfini, que l'implémentation puisse le prouver statiquement ou non.
Nous avons [intro.abstrac] p5 qui dit:
la source
La réponse principale est une idée fausse (mais courante):
Le comportement non défini est une propriété d' exécution *. Il NE PEUT PAS "voyager dans le temps"!
Certaines opérations sont définies (par la norme) comme ayant des effets secondaires et ne peuvent pas être optimisées. Les opérations qui effectuent des E / S ou qui accèdent aux
volatile
variables appartiennent à cette catégorie.Cependant , il y a une mise en garde: UB peut être n'importe quel comportement, y compris un comportement qui annule les opérations précédentes. Cela peut avoir des conséquences similaires, dans certains cas, à l'optimisation du code antérieur.
En fait, cela est cohérent avec la citation de la première réponse (c'est moi qui souligne):
Oui, cette citation ne dit « pas même en ce qui concerne les opérations précédant la première opération non définie » , mais avis que cela est spécifiquement sur le code qui est exécuté , non seulement compilé.
Après tout, un comportement indéfini qui n'est pas réellement atteint ne fait rien, et pour que la ligne contenant UB soit réellement atteinte, le code qui le précède doit s'exécuter en premier!
Donc oui, une fois UB exécuté , les effets des opérations précédentes deviennent indéfinis. Mais jusqu'à ce que cela se produise, l'exécution du programme est bien définie.
Notez, cependant, que toutes les exécutions du programme qui aboutissent à ce phénomène peuvent être optimisées pour des programmes équivalents , y compris ceux qui effectuent des opérations précédentes mais annulent ensuite leurs effets. Par conséquent, le code précédent peut être optimisé chaque fois que cela équivaudrait à ce que leurs effets soient annulés ; sinon, il ne peut pas. Voir ci-dessous pour un exemple.
* Remarque: ce n'est pas incompatible avec UB qui se produit au moment de la compilation . Si le compilateur peut effectivement prouver que le code UB sera toujours exécuté pour toutes les entrées, alors UB peut s'étendre jusqu'au moment de la compilation. Cependant, cela nécessite de savoir que tout le code précédent revient finalement , ce qui est une exigence forte. Encore une fois, voir ci-dessous pour un exemple / une explication.
Pour rendre cela concret, notez que le code suivant doit s'imprimer
foo
et attendre votre entrée quel que soit le comportement non défini qui le suit:printf("foo"); getchar(); *(char*)1 = 1;
Cependant, notez également qu'il n'y a aucune garantie qui
foo
restera à l'écran après que l'UB se produise, ou que le caractère que vous avez tapé ne sera plus dans la mémoire tampon d'entrée; ces deux opérations peuvent être «annulées», ce qui a un effet similaire au «voyage dans le temps» UB.Si la
getchar()
ligne n'était pas là, il serait légal que les lignes soient optimisées si et seulement si cela ne pouvait pas être distingué de la sortiefoo
puis de la «non-exécution».Que les deux soient indiscernables ou non dépendrait entièrement de l'implémentation (c'est-à-dire de votre compilateur et de votre bibliothèque standard). Par exemple, pouvez-vous
printf
bloquer votre thread ici en attendant qu'un autre programme lise la sortie? Ou reviendra-t-il immédiatement?S'il peut bloquer ici, alors un autre programme peut refuser de lire sa sortie complète, et il peut ne jamais revenir, et par conséquent UB peut ne jamais se produire réellement.
S'il peut revenir immédiatement ici, alors nous savons qu'il doit revenir, et par conséquent, l'optimiser est totalement impossible à distinguer de son exécution et de son annulation.
Bien sûr, puisque le compilateur sait quel comportement est autorisé pour sa version particulière de
printf
, il peut optimiser en conséquence, et par conséquentprintf
peut être optimisé dans certains cas et pas dans d'autres. Mais, encore une fois, la justification est que cela ne se distingue pas des opérations précédentes annulées par l'UB, non pas que le code précédent est "empoisonné" à cause de l'UB.la source