web-dev-qa-db-fra.com

La norme C ++ permet-elle à un booléen non initialisé de planter un programme?

Je sais qu'un "comportement indéfini" en C++ peut à peu près permettre au compilateur de faire tout ce qu'il veut. Cependant, j'ai eu un crash qui m'a surpris, car j'ai supposé que le code était suffisamment sûr.

Dans ce cas, le vrai problème ne s'est produit que sur une plate-forme spécifique à l'aide d'un compilateur spécifique, et uniquement si l'optimisation a été activée.

J'ai essayé plusieurs choses afin de reproduire le problème et de le simplifier au maximum. Voici un extrait d'une fonction appelée Serialize, qui prendrait un paramètre bool et copierait la chaîne true ou false dans un tampon de destination existant.

Cette fonction serait-elle dans une révision de code, il n'y aurait aucun moyen de dire qu'elle pourrait en fait planter si le paramètre bool était une valeur non initialisée?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Si ce code est exécuté avec les optimisations de clang 5.0.0 +, il peut/peut se bloquer.

L'opérateur ternaire attendu boolValue ? "true" : "false" semblait assez sûr pour moi, je supposais, "Quelle que soit la valeur de la poubelle dans boolValue n'a pas d'importance, car elle sera évaluée comme vraie ou fausse de toute façon."

J'ai configuré un exemple de l'explorateur du compilateur qui montre le problème dans le démontage, voici l'exemple complet. Remarque: afin de reprocher le problème, la combinaison que j'ai trouvée fonctionnait en utilisant Clang 5.0.0 avec l'optimisation -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

Le problème se pose à cause de l'optimiseur: il était assez intelligent pour déduire que les chaînes "true" et "false" ne diffèrent qu'en longueur par 1. Donc, au lieu de vraiment calculer la longueur, il utilise la valeur du bool lui-même, qui = devrait être techniquement soit 0 ou 1, et va comme ceci:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Bien que cela soit "intelligent", pour ainsi dire, ma question est: La norme C++ permet-elle à un compilateur de supposer qu'un booléen ne peut avoir qu'une représentation numérique interne de '0' ou '1' et l'utiliser de cette manière?

Ou s'agit-il d'un cas défini par l'implémentation, auquel cas l'implémentation a supposé que tous ses bools ne contiendront jamais que 0 ou 1, et toute autre valeur est un territoire de comportement indéfini?

495
Remz

Oui, ISO C++ autorise (mais n'exige pas) les implémentations pour faire ce choix.

Mais notez également que ISO C++ permet à un compilateur d'émettre du code qui se bloque exprès (par exemple avec une instruction illégale) si le programme rencontre UB, par exemple comme un moyen de vous aider à trouver des erreurs. (Ou parce que c'est une DeathStation 9000. Être strictement conforme n'est pas suffisant pour qu'une implémentation C++ soit utile dans un but réel). ISO C++ permettrait donc à un compilateur de faire un asm qui s'est écrasé (pour des raisons totalement différentes) même sur un code similaire qui lit un uint32_t non initialisé. Même si cela est requis être un type à disposition fixe sans représentation d'interruption.

C'est une question intéressante sur le fonctionnement des implémentations réelles, mais n'oubliez pas que même si la réponse était différente, votre code serait toujours dangereux car le C++ moderne n'est pas une version portable du langage d'assemblage.


Vous compilez pour le x86-64 System V ABI , qui spécifie qu'un bool en tant que fonction arg dans un registre est représenté par le modèles de bits false=0 et true=1 dans les 8 bits de poids faible du registre1. En mémoire, bool est un type à 1 octet qui doit à nouveau avoir une valeur entière de 0 ou 1.

(Un ABI est un ensemble de choix d'implémentation sur lesquels les compilateurs de la même plate-forme s'accordent pour qu'ils puissent créer du code qui appelle les fonctions des autres, y compris les tailles de type, les règles de disposition de structure et les conventions d'appel)

ISO C++ ne le spécifie pas, mais cette décision ABI est répandue car elle rend la conversion bool-> int bon marché (juste une extension zéro) . Je ne connais aucun ABI qui ne laisse pas le compilateur assumer 0 ou 1 pour bool, pour n'importe quelle architecture (pas seulement x86). Il permet des optimisations comme !mybool avec xor eax,1 pour inverser le bit bas: Tout code possible qui peut inverser un bit/entier/booléen entre 0 et 1 dans une seule instruction CP . Ou en compilant a&&b en AND au niveau du bit pour les types bool. Certains compilateurs profitent réellement Les valeurs booléennes sont de 8 bits dans les compilateurs. Les opérations sur eux sont-elles inefficaces? .

En général, la règle comme si permettait au compilateur de tirer parti des choses qui sont vraies sur la plate-forme cible en cours de compilation, car le résultat final sera un code exécutable qui implémentera le même comportement visible de l'extérieur que la source C++. (Avec toutes les restrictions que le comportement indéfini place sur ce qui est réellement "visible de l'extérieur": pas avec un débogueur, mais à partir d'un autre thread dans un programme C++ bien formé/légal.)

Le compilateur est définitivement autorisé à tirer pleinement parti d'une garantie ABI dans son code-gen, et à créer du code comme vous l'avez trouvé, qui optimise strlen(whichString) pour
5U - boolValue.
(BTW, cette optimisation est un peu intelligente, mais peut-être à courte vue par rapport à la ramification et à l'insertion memcpyen tant que magasins de données immédiates2.)

Ou le compilateur aurait pu créer une table de pointeurs et l'indexer avec la valeur entière de bool, en supposant à nouveau qu'il s'agissait d'un 0 ou 1. ( Cette possibilité est ce que la réponse de @ Barmar a suggéré =.)


Votre constructeur __attribute((noinline)) avec l'optimisation activée a conduit à cliqueter juste en chargeant un octet de la pile pour l'utiliser comme uninitializedBool. Il a fait de la place pour l'objet dans main avec Push rax (qui est plus petit et pour diverses raisons à peu près aussi efficace que sub rsp, 8), donc tout ce que les ordures étaient dans AL à l'entrée de main est la valeur utilisée pour uninitializedBool. C'est pourquoi vous avez en fait obtenu des valeurs qui n'étaient pas seulement 0.

5U - random garbage peut facilement encapsuler dans une grande valeur non signée, conduisant memcpy à entrer dans la mémoire non mappée. La destination est en stockage statique, pas la pile, donc vous n'écrasez pas une adresse de retour ou quelque chose.


D'autres implémentations pourraient faire des choix différents, par exemple false=0 et true=any non-zero value. Alors clang ne produirait probablement pas de code qui se bloque pour ceci instance spécifique d'UB. (Mais il serait toujours autorisé s'il le voulait.) Je ne connais pas d'implémentations qui choisissent autre chose que ce que x86-64 fait pour bool, mais le La norme C++ permet beaucoup de choses que personne ne fait ou ne voudrait même faire sur du matériel qui ressemble à des processeurs actuels.

ISO C++ ne précise pas ce que vous trouverez lorsque vous examinerez ou modifierez la représentation objet d'un bool. (par exemple, en memcpyen saisissant bool dans unsigned char, ce que vous êtes autorisé à faire car char* peut tout alias. Et unsigned char est garanti sans bits de remplissage, donc la norme C++ vous permet formellement de représentations d'objets hexdump sans UB. La conversion du pointeur pour copier la représentation d'objet est différente de l'attribution de char foo = my_bool, bien sûr, donc la booléenisation à 0 ou 1 ne se produirait pas et vous obtiendriez la représentation d'objet brute.)

Vous avez partiellement "caché" l'UB sur ce chemin d'exécution du compilateur avec noinline . Même si elle n'est pas en ligne, cependant, les optimisations interprocédurales pourraient toujours créer une version de la fonction qui dépend de la définition d'une autre fonction. (Premièrement, clang crée un exécutable, pas une bibliothèque partagée Unix où l'interposition de symboles peut se produire. Deuxièmement, la définition à l'intérieur de la définition class{} donc toutes les unités de traduction doivent avoir la même définition. Comme avec le mot clé inline .)

Ainsi, un compilateur pourrait émettre juste un ret ou ud2 (instruction illégale) comme définition pour main, car le chemin d'exécution commençant en haut de main rencontre inévitablement un comportement indéfini. (que le compilateur peut voir au moment de la compilation s'il décide de suivre le chemin à travers le constructeur non en ligne.)

Tout programme qui rencontre UB est totalement indéfini pour toute son existence. Mais UB à l'intérieur d'une fonction ou d'une branche if() qui ne s'exécute jamais ne corrompe pas le reste du programme. En pratique, cela signifie que les compilateurs peuvent décider d'émettre une instruction illégale, ou un ret, ou de ne rien émettre et de tomber dans le bloc/fonction suivant, pour le bloc de base complet qui peut être prouvé au moment de la compilation pour contenir ou conduire à UB.

GCC et Clang dans la pratique faire émettent parfois parfois ud2 sur UB, au lieu d'essayer même de générer du code pour des chemins d'exécution qui n'ont aucun sens. Ou pour des cas comme tomber de la fin d'une fonction nonvoid, gcc omettra parfois une instruction ret. Si vous pensiez que "ma fonction ne fera que revenir avec les ordures dans RAX", vous vous trompez profondément. Les compilateurs C++ modernes ne traitent plus le langage comme un langage d'assemblage portable. Votre programme doit vraiment être valide en C++, sans faire d'hypothèses sur l'apparence d'une version autonome non alignée de votre fonction

Un autre exemple amusant est Pourquoi l'accès non aligné à la mémoire mmap est-il parfois un défaut de segmentation sur AMD64? . x86 ne fait pas défaut sur les entiers non alignés, non? Alors pourquoi un uint16_t* mal aligné serait-il un problème? Parce que alignof(uint16_t) == 2, et violer cette hypothèse a conduit à une erreur de segmentation lors de la vectorisation automatique avec SSE2.

Voir aussi Ce que tout programmeur C devrait savoir sur le comportement indéfini # 1/ , un article d'un développeur clang.

Point clé: si le compilateur a remarqué l'UB au moment de la compilation, il pourrait "casser" (émettre un asm surprenant) le chemin à travers votre code qui provoque UB même s'il cible un ABI où n'importe quel motif binaire est une représentation d'objet valide pour bool.

Attendez-vous à une hostilité totale envers de nombreuses erreurs de la part du programmeur, en particulier les choses que les compilateurs modernes mettent en garde. C'est pourquoi vous devez utiliser -Wall et corriger les avertissements. C++ n'est pas un langage convivial, et quelque chose en C++ peut être dangereux même s'il serait sûr en asm sur la cible pour laquelle vous compilez. (par exemple, le débordement signé est UB en C++ et les compilateurs supposeront que cela ne se produit pas, même lors de la compilation pour le complément x86 à 2, sauf si vous utilisez clang/gcc -fwrapv.)

L'UB visible à la compilation est toujours dangereux, et il est vraiment difficile d'être sûr (avec l'optimisation de la liaison) que vous avez vraiment caché l'UB au compilateur et pouvez donc raisonner sur le type d'asm qu'il générera.

Ne pas être trop dramatique; Souvent, les compilateurs vous laissent échapper certaines choses et émettent du code comme vous vous y attendez même lorsque quelque chose est UB. Mais peut-être que ce sera un problème à l'avenir si les développeurs du compilateur implémentent une optimisation qui obtient plus d'informations sur les plages de valeurs (par exemple, qu'une variable n'est pas négative, lui permettant peut-être d'optimiser l'extension de signe pour libérer l'extension zéro sur x86- 64). Par exemple, dans gcc et clang actuels, faire tmp = a+INT_MIN n'optimise pas a<0 comme toujours-faux, seulement que tmp est toujours négatif. (Parce que INT_MIN + a=INT_MAX est négatif sur la cible du complément de ce 2, et a ne peut pas être plus élevé que cela.)

Donc gcc/clang ne fait pas de retour en arrière pour dériver les informations de plage pour les entrées d'un calcul, uniquement sur les résultats basés sur l'hypothèse d'aucun débordement signé: exemple sur Godbolt . Je ne sais pas si cette optimisation est intentionnellement "manquée" au nom de la convivialité ou quoi.

Notez également que les implémentations (aka compilateurs) sont autorisées à définir un comportement que ISO C++ laisse non défini . Par exemple, tous les compilateurs qui prennent en charge les intrinsèques d'Intel (comme _mm_add_ps(__m128, __m128) pour la vectorisation SIMD manuelle) doivent permettre de former des pointeurs mal alignés, ce qui est UB en C++ même si vous ne faites pas = les déréférencer. __m128i _mm_loadu_si128(const __m128i *) effectue des charges non alignées en prenant un arg __m128i* non aligné, pas un void* ou char*. Est-ce que `reinterpret_cast`ing entre le pointeur vectoriel matériel et le type correspondant est un comportement indéfini?

GNU C/C++ définit également le comportement de décalage à gauche d'un nombre signé négatif (même sans -fwrapv), séparément des règles UB normales de débordement signé. ( C'est UB dans ISO C++ , tandis que les décalages à droite des nombres signés sont définis par l'implémentation (logique vs arithmétique); les implémentations de bonne qualité choisissent l'arithmétique sur HW qui a des décalages à droite arithmétiques, mais ISO C++ ne fait pas '' t spécifier). Ceci est documenté dans la section Integer du manuel GCC , ainsi que la définition du comportement défini par l'implémentation que les normes C nécessitent que les implémentations définissent d'une manière ou d'une autre.

Il y a certainement des problèmes de qualité de mise en œuvre qui intéressent les développeurs de compilateurs; ils ne sont généralement pas en train d'essayer pour rendre les compilateurs intentionnellement hostiles, mais tirer parti de tous les nids-de-poule UB en C++ (sauf ceux qu'ils choisissent de définir) pour mieux l'optimiser peut parfois être presque impossible à distinguer .


Note de bas de page 1 : Les 56 bits supérieurs peuvent être des ordures que l'appelé doit ignorer, comme d'habitude pour les types plus étroits qu'un registre.

( Autres ABI do faites ici des choix différents . Certains nécessitent des types entiers étroits à zéro ou à extension de signe pour remplir un registre lorsqu'ils sont passés ou renvoyés par des fonctions, comme MIPS64 et PowerPC64. Voir la dernière section de cette réponse x86-64 qui compare avec ces ISA antérieures .)

Par exemple, un appelant peut avoir calculé a & 0x01010101 dans RDI et l'utiliser pour autre chose, avant d'appeler bool_func(a&1). L'appelant pourrait optimiser le &1 car il l'a déjà fait pour l'octet bas dans le cadre de and edi, 0x01010101, et il sait que l'appelé doit ignorer les octets élevés.

Ou si un booléen est passé comme 3e argument, peut-être qu'un appelant optimisant pour la taille du code le charge avec mov dl, [mem] au lieu de movzx edx, [mem], économisant 1 octet au prix d'une fausse dépendance sur l'ancienne valeur de RDX (ou autre effet de registre partiel , selon le modèle de CPU). Ou pour le premier argument, mov dil, byte [r10] au lieu de movzx edi, byte [r10], car les deux nécessitent de toute façon un préfixe REX.

C'est pourquoi clang émet movzx eax, dil dans Serialize, au lieu de sub eax, edi. (Pour les arguments entiers, clang viole cette règle ABI, en fonction du comportement non documenté de gcc et clang à zéro ou à extension de signe des entiers étroits à 32 bits. n signe ou une extension zéro est-il requis lors de l'ajout d'un décalage 32 bits vers un pointeur pour l'ABI x86-64? Donc j'étais intéressé de voir qu'il ne fait pas la même chose pour bool.)


Note de bas de page 2: Après le branchement, vous auriez juste un 4 octets mov- immédiat, ou un 4 octets + 1- magasin d'octets. La longueur est implicite dans les largeurs de magasin + décalages.

OTOH, glibc memcpy fera deux chargements/magasins de 4 octets avec un chevauchement qui dépend de la longueur, donc cela finit vraiment par rendre le tout exempt de branches conditionnelles sur le booléen. Voir le bloc L(between_4_7): dans memcpy/memmove de glibc. Ou du moins, procédez de la même manière pour chaque booléen dans la branche de memcpy pour sélectionner une taille de bloc.

En cas d'insertion, vous pouvez utiliser 2x mov- immédiat + cmov et un décalage conditionnel, ou vous pouvez laisser les données de chaîne en mémoire.

Ou si le réglage pour Intel Ice Lake ( avec la fonction Fast Short REP MOV ), un rep movsb réel pourrait être optimal. glibc memcpy pourrait commencer à utiliser rep movsb pour les petites tailles sur les processeurs avec cette fonctionnalité, économisant ainsi beaucoup de branchements.


Outils de détection d'UB et d'utilisation de valeurs non initialisées

Dans gcc et clang, vous pouvez compiler avec -fsanitize=undefined pour ajouter une instrumentation d'exécution qui avertira ou générera une erreur sur UB qui se produit lors de l'exécution. Cependant, cela n'acceptera pas les variables unitarisées. (Parce qu'il n'augmente pas la taille des caractères pour faire de la place pour un bit "non initialisé").

Voir https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

Pour trouver l'utilisation des données non initialisées, il y a l'assainisseur d'adresses et l'assainisseur de mémoire dans clang/LLVM. https://github.com/google/ sanitizers/wiki/MemorySanitizer montre des exemples de clang -fsanitize=memory -fPIE -pie détectant des lectures de mémoire non initialisées. Cela pourrait fonctionner mieux si vous compilez l'optimisation sans, donc toutes les lectures de variables finissent par se charger réellement à partir de la mémoire dans l'asm. Ils montrent qu'il est utilisé à -O2 dans un cas où la charge ne serait pas optimisée. Je ne l'ai pas essayé moi-même. (Dans certains cas, par exemple en n'initialisant pas un accumulateur avant de sommer un tableau, clang -O3 émettra du code qui résume dans un registre vectoriel qu'il n'a jamais initialisé. Ainsi, avec l'optimisation, vous pouvez avoir un cas où il n'y a pas de lecture de mémoire associée à l'UB . Mais -fsanitize=memory modifie l'asm généré et peut entraîner une vérification.)

Il tolérera la copie de la mémoire non initialisée, ainsi que les opérations logiques et arithmétiques simples avec elle. En général, MemorySanitizer suit silencieusement la propagation des données non initialisées en mémoire et signale un avertissement lorsqu'une branche de code est prise (ou non) en fonction d'une valeur non initialisée.

MemorySanitizer implémente un sous-ensemble de fonctionnalités trouvées dans Valgrind (outil Memcheck).

Cela devrait fonctionner dans ce cas car l'appel à glibc memcpy avec un length calculé à partir de la mémoire non initialisée entraînera (à l'intérieur de la bibliothèque) une branche basée sur length. S'il avait intégré une version entièrement sans branche qui n'utilisait que cmov, l'indexation et deux magasins, cela n'aurait peut-être pas fonctionné.

memcheck de Valgrind recherchera également ce type de problème, ne se plaignant pas non plus si le programme copie simplement des données non initialisées. Mais il dit qu'il détectera quand un "saut ou déplacement conditionnel dépend de valeurs non initialisées", pour essayer de détecter tout comportement visible de l'extérieur qui dépend de données non initialisées.

Peut-être que l'idée de ne pas signaler uniquement une charge est que les structures peuvent avoir un remplissage, et la copie de la structure entière (y compris le remplissage) avec un large chargement vectoriel/magasin n'est pas une erreur même si les membres individuels n'ont été écrits qu'un par un. Au niveau asm, les informations sur ce qui était du remplissage et ce qui fait réellement partie de la valeur ont été perdues.

279
Peter Cordes

Le compilateur peut supposer qu'une valeur booléenne passée en argument est une valeur booléenne valide (c'est-à-dire qui a été initialisée ou convertie en true ou false). La valeur true ne doit pas nécessairement être la même que l'entier 1 - en effet, il peut y avoir différentes représentations de true et false - mais le paramètre doit être une représentation valide de l'une de ces deux valeurs, où "représentation valide" est définie par l'implémentation.

Donc, si vous ne parvenez pas à initialiser un bool, ou si vous réussissez à l'écraser via un pointeur d'un type différent, les hypothèses du compilateur seront erronées et un comportement indéfini s'ensuivra. Vous aviez été prévenu:

50) L'utilisation d'une valeur booléenne de la manière décrite par la présente Norme internationale comme "non définie", par exemple en examinant la valeur d'un objet automatique non initialisé, pourrait le faire se comporter comme s'il n'était ni vrai ni faux. (Note de bas de page du paragraphe 6 du §6.9.1, Types fondamentaux)

56
rici

La fonction elle-même est correcte, mais dans votre programme de test, l'instruction qui appelle la fonction provoque un comportement non défini en utilisant la valeur d'une variable non initialisée.

Le bogue se trouve dans la fonction appelante et il pourrait être détecté par un examen du code ou une analyse statique de la fonction appelante. En utilisant le lien de votre explorateur de compilateur, le compilateur gcc 8.2 détecte le bogue. (Peut-être pourriez-vous déposer un rapport de bogue contre clang qu'il ne trouve pas le problème).

Un comportement indéfini signifie que n'importe quoi peut se produire, ce qui inclut le programme qui plante quelques lignes après l'événement qui a déclenché le comportement indéfini.

NB. La réponse à "Un comportement indéfini peut-il provoquer _____?" est toujours "Oui". C'est littéralement la définition d'un comportement indéfini.

52
M.M

Un booléen est uniquement autorisé à contenir les valeurs dépendant de l'implémentation utilisées en interne pour true et false, et le code généré peut supposer qu'il ne contiendra qu'une seule de ces deux valeurs.

En règle générale, l'implémentation utilise l'entier 0 Pour false et 1 Pour true, pour simplifier les conversions entre bool et int, et faire if (boolvar) générer le même code que if (intvar). Dans ce cas, on peut imaginer que le code généré pour le ternaire dans l'affectation utiliserait la valeur comme index dans un tableau de pointeurs vers les deux chaînes, c'est-à-dire qu'il pourrait être converti en quelque chose comme:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Si boolValue n'est pas initialisé, il pourrait en fait contenir n'importe quelle valeur entière, ce qui entraînerait alors un accès en dehors des limites du tableau strings.

23
Barmar

Résumant souvent votre question, vous vous demandez si la norme C++ permet à un compilateur de supposer qu'un bool ne peut avoir qu'une représentation numérique interne de '0' ou '1' et de l'utiliser de cette manière?

La norme ne dit rien sur la représentation interne d'un bool. Il définit uniquement ce qui se passe lors de la conversion d'un bool en int (ou vice versa). Surtout, en raison de ces conversions intégrales (et du fait que les gens en dépendent assez), le compilateur utilisera 0 et 1, mais il n'est pas obligé (bien qu'il doive respecter les contraintes de tout ABI de niveau inférieur qu'il utilise ).

Ainsi, le compilateur, lorsqu'il voit un bool est en droit de considérer que ledit bool contient l'un des modèles de bits 'true' ou 'false' et faites tout ce que vous ressentez. Donc, si les valeurs de true et false sont respectivement 1 et 0, le compilateur est en effet autorisé à optimiser strlen en 5 - <boolean value>. D'autres comportements amusants sont possibles!

Comme indiqué à plusieurs reprises ici, un comportement indéfini a des résultats indéfinis. Y compris, mais sans s'y limiter

  • Votre code fonctionne comme prévu
  • Votre code échoue à des moments aléatoires
  • Votre code n'est pas exécuté du tout.

Voir Ce que tout programmeur doit savoir sur le comportement indéfini

15
Tom Tanner