web-dev-qa-db-fra.com

Est-il légal que le code source contenant un comportement non défini plante le compilateur?

Disons que je vais compiler du code source C++ mal écrit qui invoque un comportement non défini, et donc (comme on dit) "tout peut arriver".

Du point de vue de ce que la spécification du langage C++ juge acceptable dans un compilateur "conforme", "quoi" que ce soit dans ce scénario inclut le crash du compilateur (ou le vol de mes mots de passe, ou tout autre mauvais comportement ou erreur lors de la compilation), ou est portée du comportement undefined limité spécifiquement à ce qui peut se produire lorsque l'exécutable résultant s'exécute?

84
Jeremy Friesner

La définition normative d'un comportement indéfini est la suivante:

[defns.undefined]

comportement pour lequel la présente Norme internationale n'impose aucune exigence

[Remarque: Un comportement indéfini peut être attendu lorsque la présente Norme internationale omet toute définition explicite de comportement ou lorsqu'un programme utilise une construction ou des données erronées. Les comportements indéfinis autorisés vont de l'ignorance complète de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans l'émission d'un message de diagnostic), à la fin d'une traduction ou de l'exécution (avec l'émission d'un message de diagnostic). De nombreuses constructions de programme erronées n'engendrent pas de comportement indéfini; ils doivent être diagnostiqués. L'évaluation d'une expression constante ne présente jamais de comportement explicitement spécifié comme non défini. - note de fin]

Bien que la note elle-même ne soit pas normative, elle décrit une gamme de comportements que les implémentations sont connues pour présenter. Donc, planter le compilateur (qui se termine brutalement par la traduction), est légitime selon cette note. Mais vraiment, comme le dit le texte normatif, la norme ne place aucune limite pour l'exécution ou la traduction. Si une implémentation vole vos mots de passe, ce n'est pas une violation de tout contrat énoncé dans la norme.

La plupart des types d'UB qui nous inquiètent habituellement, comme NULL-deref ou diviser par zéro, sont runtime UB. La compilation d'une fonction qui provoquerait l'exécution UB si elle est exécutée ne doit pas provoquer le plantage du compilateur. À moins qu'il ne puisse peut-être prouver que la fonction (et ce chemin à travers le fonction) définitivement sera être exécuté par le programme.

(2e réflexion: peut-être que je n'ai pas considéré l'évaluation requise par template/constexpr au moment de la compilation. Peut-être que UB pendant cela est autorisé à provoquer une bizarrerie arbitraire pendant la traduction même si la fonction résultante n'est jamais appelée.)

La partie se comportant pendant la traduction de la citation ISO C++ dans la réponse de @ StoryTeller est similaire au langage utilisé dans la norme ISO C. C n'inclut pas de modèles ou constexpr eval obligatoire au moment de la compilation.

Mais fait amusant : ISO C dit dans une note que si la traduction est terminée, ce doit être avec un message de diagnostic. Ou "se comporter pendant la traduction ... de manière documentée". Je ne pense pas que "ignorer complètement la situation" puisse être interprété comme incluant l'arrêt de la traduction.


Ancienne réponse, écrite avant que j'apprenne l'UB au moment de la traduction. C'est vrai pour runtime-UB, et donc potentiellement toujours utile.


Il n'y a rien de tel que UB qui arrive au moment de la compilation. Il peut être visible pour le compilateur le long d'un certain chemin d'exécution, mais en termes C++ il n'a pas arrivé jusqu'à ce que l'exécution atteigne ce chemin d'exécution via un fonction.

Les défauts dans un programme qui rendent impossible la compilation ne sont même pas UB, ce sont des erreurs de syntaxe. Un tel programme n'est "pas bien formé" dans la terminologie C++ (si j'ai mon correct standard). Un programme peut être bien formé mais contenir de l'UB. Différence entre un comportement indéfini et mal formé, aucun message de diagnostic requis

À moins que je ne comprenne quelque chose, ISO C++ requiert ce programme à compiler et à exécuter correctement, car l'exécution n'atteint jamais la division par zéro. (En pratique ( Godbolt ), les bons compilateurs ne font que des exécutables de travail. Gcc/clang avertit de x / 0 Mais pas de cela, même lors de l'optimisation. Mais de toute façon, nous essayons de dire comment faible ISO C++ permet à la qualité de l'implémentation d'être. Donc, vérifier gcc/clang n'est guère un test utile autre que pour confirmer que j'ai bien écrit le programme.)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

Un cas d'utilisation pourrait impliquer le préprocesseur C, ou les variables constexpr et la ramification de ces variables, ce qui conduit à un non-sens dans certains chemins qui ne sont jamais atteints pour ces choix de constantes.

Les chemins d'exécution qui provoquent une UB visible à la compilation peuvent être supposés ne jamais être empruntés, par exemple un compilateur pour x86 pourrait émettre un ud2 (provoquer une exception d'instruction illégale) comme définition pour cause_UB(). Ou dans une fonction, si un côté d'une if() conduit à prouvable UB, la branche peut être supprimée.

Mais le compilateur doit encore tout compiler sinon d'une manière saine et correcte. Tous les chemins que ne faites pas rencontrent (ou ne peuvent pas prouver) UB doivent toujours être compilés en asm qui s'exécute comme si la machine abstraite C++ l'exécutait.


Vous pourriez faire valoir que l'UB inconditionnelle visible au moment de la compilation dans main est une exception à cette règle. Ou sinon prouvable à la compilation cette exécution commençant à main atteint en fait l'UB garantie.

Je dirais toujours que les comportements légaux du compilateur incluent la production d'une grenade qui explose si run. Ou plus vraisemblablement, une définition de main qui consiste en une seule instruction illégale. Je dirais que si vous jamais exécutez le programme, il n'y a pas encore eu d'UB. Le compilateur lui-même n'est pas autorisé à exploser, OMI.


Fonctions contenant UB possible ou prouvable à l'intérieur des branches

UB le long d'un chemin d'exécution donné recule dans le temps pour "contaminer" tout le code précédent. Mais dans la pratique, les compilateurs ne peuvent tirer parti de cette règle que lorsqu'ils peuvent réellement prouver que les chemins d'exécution mènent à l'UB visible à la compilation. par exemple.

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

Le compilateur doit créer un asm qui fonctionne pour tous les x autres que 3, jusqu'au point où x * 5 Provoque un débordement signé UB à INT_MIN et INT_MAX. Si cette fonction n'est jamais appelée avec x==3, Le programme ne contient bien sûr pas d'UB et doit fonctionner comme écrit.

Nous pourrions aussi bien avoir écrit if(x == 3) __builtin_unreachable(); dans GNU C pour dire au compilateur que x n'est certainement pas 3.

Dans la pratique, il y a du code "champ de mines" partout dans les programmes normaux. par exemple. toute division par un entier promet au compilateur qu'elle est non nulle. Tout pointeur deref promet au compilateur qu'il n'est pas NULL.

8
Peter Cordes

Que signifie "légal" ici? Tout ce qui ne contredit pas la norme C ou la norme C++ est légal, selon ces normes. Si vous exécutez une instruction i = i++; et par conséquent les dinosaures envahissent le monde, ce qui ne contredit pas les normes. Cela contredit cependant les lois de la physique, donc ça ne va pas arriver :-)

Si un comportement non défini plante votre compilateur, cela ne viole pas la norme C ou C++. Cela signifie cependant que la qualité du compilateur pourrait (et devrait probablement) être améliorée.

Dans les versions précédentes de la norme C, il y avait des instructions qui étaient des erreurs ou ne dépendaient pas d'un comportement non défini:

char* p = 1 / 0;

L'affectation d'une constante 0 à un caractère * est autorisée. Autoriser une constante non nulle ne l'est pas. Étant donné que la valeur de 1/0 est un comportement indéfini, il s'agit d'un comportement indéfini, que le compilateur accepte ou non cette instruction. (De nos jours, 1/0 ne correspond plus à la définition d '"expression constante entière").

3
gnasher729