Considérez la déclaration suivante:
*((char*)NULL) = 0; //undefined behavior
Il invoque clairement un comportement indéfini. L'existence d'une telle instruction dans un programme donné signifie-t-elle que l'ensemble du programme est indéfini ou que le comportement ne devient indéfini qu'une fois que le flux de contrôle atteint cette instruction?
Le programme suivant serait-il bien défini au cas où l'utilisateur n'entrerait jamais le numéro 3
?
while (true) {
int num = ReadNumberFromConsole();
if (num == 3)
*((char*)NULL) = 0; //undefined behavior
}
Ou s'agit-il d'un comportement totalement indéfini, peu importe ce que l'utilisateur entre?
De plus, le compilateur peut-il supposer qu'un comportement non défini ne sera jamais exécuté lors de l'exécution? Cela permettrait de raisonner à rebours dans le temps:
int num = ReadNumberFromConsole();
if (num == 3) {
PrintToConsole(num);
*((char*)NULL) = 0; //undefined behavior
}
Ici, le compilateur pourrait penser qu'au cas où num == 3
nous invoquerions toujours un comportement non défini. Par conséquent, ce cas doit être impossible et le numéro n'a pas besoin d'être imprimé. La if
déclaration entière pourrait être optimisée. Ce type de raisonnement à rebours est-il autorisé selon la norme?
const int i = 0; if (i) 5/i;
.PrintToConsole
n'appelle passtd::exit
, il doit donc effectuer l'appel.Réponses:
Ni. La première condition est trop forte et la seconde est trop faible.
Les accès aux objets sont parfois séquencés, mais la norme décrit le comportement du programme en dehors du temps. Danvil a déjà cité:
Cela peut être interprété:
Ainsi, une instruction inaccessible avec UB ne donne pas au programme UB. Une instruction accessible qui (à cause des valeurs des entrées) n'est jamais atteinte, ne donne pas au programme UB. C'est pourquoi votre première condition est trop forte.
Maintenant, le compilateur ne peut pas en général dire ce que contient UB. Donc, pour permettre à l'optimiseur de réorganiser les instructions avec UB potentiel qui serait réordonnable si leur comportement était défini, il est nécessaire de permettre à UB de "remonter dans le temps" et de se tromper avant le point de séquence précédent (ou en C ++ 11, pour que l'UB affecte les choses qui sont séquencées avant la chose UB). Par conséquent, votre deuxième condition est trop faible.
Un exemple majeur de ceci est lorsque l'optimiseur s'appuie sur un aliasing strict. L'intérêt des règles strictes d'aliasing est de permettre au compilateur de réorganiser les opérations qui ne pourraient pas être réordonnées valablement s'il était possible que les pointeurs en question alias la même mémoire. Ainsi, si vous utilisez des pointeurs d'aliasing illégal et que UB se produit, cela peut facilement affecter une instruction "avant" l'instruction UB. En ce qui concerne la machine abstraite, l'instruction UB n'a pas encore été exécutée. En ce qui concerne le code objet réel, il a été partiellement ou entièrement exécuté. Mais la norme n'essaie pas d'entrer dans les détails sur ce que signifie pour l'optimiseur de réorganiser les instructions, ou quelles en sont les implications pour UB. Cela donne simplement à la licence d'implémentation une erreur dès qu'il le souhaite.
Vous pouvez penser à cela comme "UB a une machine à remonter le temps".
Plus précisément pour répondre à vos exemples:
PrintToConsole(3)
d'être connu pour être sûr de revenir. Cela pourrait lever une exception ou autre.Un exemple similaire à votre deuxième est l'option gcc
-fdelete-null-pointer-checks
, qui peut prendre un code comme celui-ci (je n'ai pas vérifié cet exemple spécifique, considérez-le comme illustratif de l'idée générale):et changez-le en:
Pourquoi? Parce que si
p
est nul, le code a de toute façon UB, de sorte que le compilateur peut supposer qu'il n'est pas nul et optimiser en conséquence. Le noyau Linux a trébuché là-dessus ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) essentiellement parce qu'il fonctionne dans un mode où le déréférencement d'un pointeur nul n'est pas censé be UB, cela devrait entraîner une exception matérielle définie que le noyau peut gérer. Lorsque l'optimisation est activée, gcc nécessite l'utilisation de-fno-delete-null-pointer-checks
afin de fournir cette garantie hors norme.PS La réponse pratique à la question "Quand un comportement indéfini frappe-t-il?" est "10 minutes avant votre départ pour la journée".
la source
void can_add(int x) { if (x + 100 < x) complain(); }
peut être optimisé disparaître entièrement, parce que six+100
n » a rien de trop - plein se produit, et si lex+100
fait de débordement, qui est UB selon la norme, donc rien ne peut arriver.3
si elle le souhaitait, et ranger à la maison pour la journée dès qu'elle en a vu un entrant.La norme indique à 1,9 / 4
Le point intéressant est probablement ce que signifie «contenir». Un peu plus tard, à 1,9 / 5, il déclare:
Ici, il mentionne spécifiquement "l'exécution ... avec cette entrée". J'interpréterais cela comme un comportement indéfini dans une branche possible qui n'est pas exécutée actuellement n'influence pas la branche d'exécution actuelle.
Les hypothèses basées sur un comportement indéfini lors de la génération de code constituent un autre problème. Voir la réponse de Steve Jessop pour plus de détails à ce sujet.
la source
Un exemple instructif est
Le GCC actuel et le Clang actuel optimiseront cela (sur x86) pour
car ils en déduisent
x
toujours zéro de l'UB dans leif (x)
chemin de contrôle. GCC ne vous donnera même pas d'avertissement d'utilisation d'une valeur non initialisée! (parce que la passe qui applique la logique ci-dessus s'exécute avant la passe qui génère des avertissements de valeur non initialisée)la source
a
même si dans toutes les circonstances un non initialiséa
serait passé à la fonction que la fonction ne ferait jamais rien avec elle)?Le projet de travail actuel du C ++ indique dans la version 1.9.4 que
Sur cette base, je dirais qu'un programme contenant un comportement indéfini sur n'importe quel chemin d'exécution peut faire n'importe quoi à chaque fois de son exécution.
Il y a deux très bons articles sur le comportement indéfini et ce que font habituellement les compilateurs:
la source
int f(int x) { if (x > 0) return 100/x; else return 100; }
n'invoque certainement jamais un comportement indéfini, même si elle100/0
n'est bien sûr pas définie.printf("Hello, World"); *((char*)NULL) = 0
n'est pas garanti d'imprimer quoi que ce soit. Cela facilite l'optimisation, car le compilateur peut librement réorganiser les opérations (soumises à des contraintes de dépendance, bien sûr) dont il sait qu'elles finiront par se produire, sans avoir à prendre en compte un comportement non défini.int x,y; std::cin >> x >> y; std::cout << (x+y);
est permis de dire que "1 + 1 = 17", simplement parce qu'il y a des entrées oùx+y
débordent (qui est UB puisqueint
est un type signé).Le mot «comportement» signifie que quelque chose est en train d'être fait . Un état qui n'est jamais exécuté n'est pas un «comportement».
Une illustration:
Est-ce un comportement indéfini? Supposons que nous soyons sûrs à 100%
ptr == nullptr
à au moins une fois pendant l'exécution du programme. La réponse devrait être oui.Et ça?
Est-ce indéfini? (Rappelez-vous
ptr == nullptr
au moins une fois?) J'espère bien que non, sinon vous ne pourrez pas du tout écrire de programme utile.Aucun srandardese n'a été blessé dans l'élaboration de cette réponse.
la source
Le comportement non défini survient lorsque le programme provoquera un comportement non défini, quoi qu'il arrive ensuite. Cependant, vous avez donné l'exemple suivant.
Sauf si le compilateur connaît la définition de
PrintToConsole
, il ne peut pas supprimerif (num == 3)
conditionnel. Supposons que vous ayezLongAndCamelCaseStdio.h
un en-tête système avec la déclaration suivante dePrintToConsole
.Rien de bien utile, d'accord. Maintenant, voyons à quel point le vendeur est mauvais (ou peut-être pas si mauvais, un comportement indéfini), en vérifiant la définition réelle de cette fonction.
Le compilateur doit en fait supposer que toute fonction arbitraire dont le compilateur ne sait pas ce qu'il fait peut quitter ou lancer une exception (dans le cas de C ++). Vous pouvez remarquer que
*((char*)NULL) = 0;
cela ne sera pas exécuté, car l'exécution ne continuera pas après l'PrintToConsole
appel.Le comportement indéfini frappe lorsque
PrintToConsole
revient réellement. Le compilateur s'attend à ce que cela ne se produise pas (car cela amènerait le programme à exécuter un comportement indéfini quoi qu'il arrive), donc tout peut arriver.Cependant, considérons autre chose. Disons que nous faisons une vérification nulle et que nous utilisons la variable après une vérification nulle.
Dans ce cas, il est facile de remarquer que cela
lol_null_check
nécessite un pointeur non NULL. L'affectation à lawarning
variable non volatile globale n'est pas quelque chose qui pourrait quitter le programme ou lever une exception. Lepointer
est également non volatile, donc il ne peut pas changer sa valeur par magie au milieu de la fonction (si c'est le cas, c'est un comportement indéfini). Appellol_null_check(NULL)
entraînera un comportement indéfini qui peut empêcher l'affectation de la variable (car à ce stade, le fait que le programme exécute le comportement non défini est connu).Cependant, le comportement non défini signifie que le programme peut tout faire. Par conséquent, rien n'empêche le comportement non défini de remonter dans le temps et de planter votre programme avant la première ligne d'
int main()
exécutions. C'est un comportement indéfini, cela n'a pas à avoir de sens. Il peut tout aussi bien planter après avoir tapé 3, mais le comportement non défini remontera dans le temps et plantera avant même que vous ne tapiez 3. Et qui sait, peut-être qu'un comportement non défini écrasera la RAM de votre système, et fera planter votre système 2 semaines plus tard, pendant que votre programme non défini n'est pas en cours d'exécution.la source
PrintToConsole
est ma tentative d'insérer un effet secondaire externe au programme qui est visible même après un crash et est fortement séquencé. Je voulais créer une situation où nous pouvons dire avec certitude si cette déclaration a été optimisée. Mais vous avez raison de dire qu'il pourrait ne jamais revenir. Votre exemple d'écriture dans un global peut être soumis à d'autres optimisations qui ne sont pas liées à UB. Par exemple, un global inutilisé peut être supprimé. Avez-vous une idée pour créer un effet secondaire externe d'une manière qui garantit le retour du contrôle?volatile
variable pourrait légitimement déclencher une opération d'E / S qui pourrait à son tour interrompre immédiatement le thread actuel; le gestionnaire d'interruption pourrait alors tuer le thread avant qu'il n'ait une chance d'effectuer autre chose. Je ne vois aucune justification par laquelle le compilateur pourrait pousser un comportement indéfini avant ce point.Si le programme atteint une instruction qui invoque un comportement non défini, aucune exigence n'est imposée à la sortie / au comportement du programme. peu importe qu'ils aient lieu "avant" ou "après" un comportement non défini est invoqué.
Votre raisonnement sur les trois extraits de code est correct. En particulier, un compilateur peut traiter toute instruction invoquant de manière inconditionnelle un comportement indéfini comme GCC le traite
__builtin_unreachable()
: comme une indication d'optimisation que l'instruction est inaccessible (et par conséquent, que tous les chemins de code qui y mènent sans condition sont également inaccessibles). D'autres optimisations similaires sont bien entendu possibles.la source
__builtin_unreachable()
commencé à avoir des effets qui se sont déroulés à la fois en arrière et en avant dans le temps? Étant donné quelque chose commeextern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }
je pourrais voir lebuiltin_unreachable()
comme étant bon de faire savoir au compilateur qu'il peut omettre l'return
instruction, mais ce serait plutôt différent de dire que le code précédent pourrait être omis.__builtin_unreachable
est atteint. Ce programme est défini.restrict
pointeur en direct , à l'aide d'ununsigned char*
.De nombreuses normes pour de nombreux types de choses consacrent beaucoup d'efforts à décrire les choses que les implémentations DEVRAIENT ou NE DOIVENT PAS faire, en utilisant une nomenclature similaire à celle définie dans IETF RFC 2119 (sans nécessairement citer les définitions de ce document). Dans de nombreux cas, les descriptions des choses que les implémentations devraient faire sauf dans les cas où elles seraient inutiles ou peu pratiques sont plus importantes que les exigences auxquelles toutes les implémentations conformes doivent se conformer.
Malheureusement, les standards C et C ++ ont tendance à éviter les descriptions de choses qui, bien qu'elles ne soient pas obligatoires à 100%, devraient néanmoins être attendues d'implémentations de qualité qui ne documentent pas un comportement contraire. Une suggestion selon laquelle les implémentations devraient faire quelque chose pourrait être considérée comme impliquant que celles qui ne le sont pas sont inférieures, et dans les cas où il serait généralement évident quels comportements seraient utiles ou pratiques, par opposition à peu pratiques et inutiles, sur une implémentation donnée, il y avait il est peu perçu que la norme interfère avec ces jugements.
Un compilateur intelligent pourrait se conformer à la norme tout en éliminant tout code qui n'aurait aucun effet sauf lorsque le code reçoit des entrées qui provoqueraient inévitablement un comportement indéfini, mais «intelligent» et «stupide» ne sont pas des antonymes. Le fait que les auteurs de la norme aient décidé qu'il pourrait y avoir certains types de mises en œuvre où un comportement utile dans une situation donnée serait inutile et irréalisable n'implique aucun jugement sur la question de savoir si de tels comportements devraient être considérés comme pratiques et utiles pour d'autres. Si une mise en œuvre pouvait maintenir une garantie comportementale sans frais au-delà de la perte d'une opportunité d'élagage "de branche morte", presque toute valeur que le code utilisateur pourrait recevoir de cette garantie dépasserait le coût de sa fourniture. L'élimination des branches mortes peut être bien dans les cas où cela ne le serait pas, mais si, dans une situation donnée, le code utilisateur aurait pu gérer presque n'importe quel comportement possible autre que l'élimination des branches mortes, tout effort que le code utilisateur aurait à dépenser pour éviter UB dépasserait probablement la valeur obtenue à partir de DBE.
la source
x*y < z
moment oùx*y
il ne déborde pas, et en cas de dépassement, le rendement est de 0 ou 1 de manière arbitraire mais sans effets secondaires, il n'y a aucune raison sur la plupart des plates-formes pour que le respect des deuxième et troisième exigences soit plus coûteux que répondre à la première, mais toute manière d'écrire l'expression pour garantir un comportement défini par Standard dans tous les cas entraînerait dans certains cas un coût important. Écrire l'expression comme(int64_t)x*y < z
pourrait plus que quadrupler le coût de calcul ...(int)((unsigned)x*y) < z
empêcher un compilateur d'employer ce qui aurait pu être des substitutions algébriques utiles (par exemple, s'il sait celax
etz
sont égaux et positifs, cela pourrait simplifier l'expression d'originey<0
, mais la version utilisant unsigned forcerait le compilateur à effectuer la multiplication). Si le compilateur peut garantir même si le Standard ne l'exige pas, il respectera l'exigence «rendement 0 ou 1 sans effets secondaires», le code utilisateur pourrait donner au compilateur des opportunités d'optimisation qu'il ne pourrait pas obtenir autrement.x*y
d'une valeur normale en cas de dépassement de capacité mais de n'importe quelle valeur. UB configurable en C / C ++ me semble important.