Est-il légal pour le code source contenant un comportement non défini de planter le compilateur?

85

Disons que je vais compiler un code source C ++ mal écrit qui invoque un comportement indé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 plantage du compilateur (ou le vol de mes mots de passe, ou autrement un mauvais comportement ou erreur au moment de la compilation) portée du comportement indéfini limité spécifiquement à ce qui peut arriver lorsque l'exécutable résultant s'exécute?

Jeremy Friesner
la source
22
"UB est UB. Vivez avec" ... Non, attendez. "Veuillez publier un MCVE." ... Non attends. J'aime la question pour tous les réflexes qu'elle déclenche de manière inappropriée. :-)
Yunnosch
14
Il n'y a vraiment aucune limitation, c'est la raison pour laquelle on dit que UB peut invoquer des démons nasaux .
Un mec programmeur
15
UB peut demander à l'auteur de poser une question sur SO. : P
Tanveer Badar
45
Indépendamment de ce que dit le standard C ++, si j'étais un rédacteur de compilateur, je le considérerais certainement comme un bogue dans mon compilateur. Donc, si vous voyez cela, déposez un rapport de défaut.
john
9
@LeifWillerts C'était dans les années 80. Je ne me souviens pas de la construction exacte, mais je pense qu'elle reposait sur l'utilisation d'un type de variable alambiqué. Après avoir mis en place un remplaçant, j'ai eu un moment "à quoi pensais-je - les choses ne fonctionnent pas comme ça". Je n'ai pas blâmé le compilateur pour avoir rejeté la construction, juste pour avoir redémarré la machine. Je doute que quiconque rencontre ce compilateur aujourd'hui. C'était le compilateur croisé HP C pour le HP 64000 ciblant le microprocesseur 68000.
Avi Berger

Réponses:

71

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

[defns.undefined]

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

[Note: Un comportement non défini peut être attendu lorsque la présente Norme internationale omet toute définition explicite de comportement ou lorsqu'un programme utilise une construction erronée ou des données erronées. Le comportement indéfini admissible va de l'ignorance totale 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 d'une 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 est la traduction se terminant brusquement), est légitime selon cette note. Mais en réalité, 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 d'un contrat énoncé dans la norme.

Conteur - Unslander Monica
la source
43
Cela dit, si vous pouvez réellement obtenir un compilateur pour exécuter du code arbitraire au moment de la compilation, sans aucun sandbox, alors diverses personnes de la sécurité seraient très intéressées d'en savoir plus. Il en va de même pour la segmentation du compilateur.
Kevin
67
Idem pour ce que Kevin a dit. En tant qu'ingénieur compilateur C / C ++ / etc dans une carrière précédente, notre position était qu'un comportement indéfini pouvait planter votre programme , bousiller vos données de sortie, mettre le feu à votre maison, peu importe. Mais le compilateur ne doit jamais planter, quelle que soit l'entrée. (Cela pourrait ne pas donner de messages d'erreur utiles, mais cela devrait produire une sorte de diagnostic et de sortie plutôt que de simplement crier CTHULHU PREND LA ROUE et segfaulting.)
Ti Strga
8
@TiStrga Je parie que Cthulhu ferait un pilote de F1 génial.
zeta-band
35
"Si une implémentation vole vos mots de passe, ce n'est pas une violation d'un contrat énoncé dans la norme." C'est vrai que le code ait ou non UB, n'est-ce pas? La norme dicte uniquement ce que le programme compilé doit faire - un compilateur qui compile correctement le code mais vole vos mots de passe dans le processus ne désobéirait pas à la norme.
Carmeister
8
@Carmeister, oooh, c'est un bon point, je vais m'assurer de le rappeler aux gens chaque fois que ces arguments "UB donne la permission au compilateur de déclencher une guerre nucléaire" apparaissent. Encore.
ilkkachu
8

La plupart des types d'UB dont nous nous soucions habituellement, comme NULL-deref ou diviser par zéro, sont des UB d' exécution . La compilation d'une fonction qui provoquerait UB d'exécution si elle était exécutée ne doit pas provoquer le plantage du compilateur. À moins que cela puisse prouver que la fonction (et ce chemin à travers la fonction) sera définitivement exécutée par le programme.

(Deuxième 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.)

Le comportement lors de la traduction de la partie 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 les modèles ou constexprl'évaluation obligatoire au moment de la compilation.

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


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


Il n'y a pas une telle chose comme 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 ++, cela ne s'est pas produit jusqu'à ce que l'exécution atteigne ce chemin d'exécution via une fonction.

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

À moins que je ne comprenne mal quelque chose, ISO C ++ nécessite que ce programme se compile et s'exécute correctement, car l'exécution n'atteint jamais la division par zéro. (Dans la pratique ( Godbolt ), un bon compilateur juste faire executables de travail. Gcc / clang mettent en garde contre x / 0mais pas, même lors de l' optimisation. Mais de toute façon, nous essayons de dire à quel point faible ISO C ++ permet la qualité de la mise en œuvre soit. Donc , la vérification gcc / clang n'est guère un test utile autre que pour confirmer que j'ai écrit le programme correctement.)

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 pour cela pourrait impliquer le préprocesseur C, ou des constexprvariables et des branchements sur ces variables, ce qui conduit à des absurdités dans certains chemins qui ne sont jamais atteints pour ces choix de constantes.

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

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


Vous pourriez affirmer que l'UB visible au moment de la compilation inconditionnelle dans mainest une exception à cette règle. Ou autrement prouvable au moment de la compilation, que l'exécution commençant à mainatteint en fait UB garanti.

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


Fonctions contenant des UB possibles ou prouvables à l'intérieur des branches

UB le long de n'importe quel chemin d'exécution donné recule dans le temps pour «contaminer» tout le code précédent. Mais en 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 à UB visible au moment de 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 xautres que 3, jusqu'aux points où x * 5provoque un dépassement de capacité UB signé à 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 aurions tout aussi bien pu écrire if(x == 3) __builtin_unreachable();en GNU C pour dire au compilateur que ce xn'est certainement pas 3.

En 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'il est différent de zéro. Tout pointeur déréf promet au compilateur qu'il n'est pas NULL.

Peter Cordes
la source
3

Que veut dire «légal» ici? Tout ce qui ne contredit pas le standard C ou C ++ est légal, selon ces standards. Si vous exécutez une déclaration i = i++;et que les dinosaures prennent le contrôle du monde, cela ne contredit pas les normes. Cela contredit cependant les lois de la physique, donc ça n'arrivera pas :-)

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, certaines instructions étaient des erreurs ou ne dépendaient pas d'un comportement indéfini:

char* p = 1 / 0;

L'attribution d'une constante 0 à un caractère * est autorisée. Autoriser une constante non nulle ne l'est pas. Puisque la valeur de 1/0 est un comportement indéfini, il s'agit d'un comportement indéfini que le compilateur doive ou non accepter cette instruction. (De nos jours, 1/0 ne répond plus à la définition d '«expression constante entière»).

gnasher729
la source
3
Pour être précis: les dinosaures envahissant le monde ne contredisent aucune loi de la physique (par exemple la variation de Jurassic Park). C'est juste hautement improbable. :)
freakish