Les Undefined Behaviors
Les Undefined Behaviors (UB) sont des situations où aucun comportement n'est imposé au compilateur. Il s'agit donc d'erreurs difficiles à découvrir puisqu'elles peuvent faire crash le programme, renvoyer des valeurs incorrectes silencieusement ou faire fonctionner correctement le programme.
Elles existent pour permettent aux compilateurs d'optimiser le code (aucune contrainte n'existe pour la situation donnée) et augmenter leur comptabilité (n'importe quel comportement est accepté). Il en existe environ 200 en C (2011), et encore bien d'autres en C++.
Par exemple, l'overflow des entiers signés est un UB :
Des overflows des nombres signés peuvent être définis avec l'option -fwrapv,
et ils peuvent faire crash le programme avec -ftrapv
.
Le comportement d'une UB est fortement lié à la plateforme, le compilateur ou les optimisations activées. Des UB peuvent donc rester invisibles jusqu'au moment où une optimisation les exploite pour faire dysfonctionner le code.
Unspecified & implementation-defined behavior
Les Unspecified Behaviors désignent des situations correctes où plusieurs solutions sont disponibles, et aucune n'est requise. Par exemple, l'ordre d'évaluation des fonctions est un Unspecified Behavior :
Les Implementation-defined Behaviors sont des comportements qui peuvent changer selon l'implémentation. Cependant, contrairement aux Unspecified Behaviors, chaque implémentation doit toujours adopter la même solution. Par exemple, la taille en mémoire d'un pointeur fait partie de cette catégorie :
UB courants
Voici des exemples qui illustre certains Undefined Behaviors communs :
L'overflow des entiers signés.
Lire un objet non initialisé.
Utiliser des pointeurs de types différents pointant vers le même objet.
Ne pas retourner de valeur dans une fonction ne retournant pas void.
Déréférencer un pointeur nul, ou vers un objet non existant (ou déjà détruit).
Manipuler le membre inactif d'une union (comportement défini en C).
Cette réponse de Stack Overflow liste quelques UB qu'il faut garder en tête. Les UB nécessitent une attention particulière pour être évitées, mais des outils permettent de les mitiger (comme des analyseurs statiques, ou UBsan qui modifie le programme à la compilation pour détecter des UB en cours d'exécution).
Optimisations possibles
Une optimisation classique effectuée par le compilateur consiste à partir du principe que les UB ne sont jamais atteints. Ainsi, lorsqu'une UB est créé, le compilateur peut partir du principe que le chemin contenant l'UB n'est jamais atteint :
Dans l'exemple ci-dessus, la boucle dépasse table et finit par accéder à de la mémoire qui ne nous appartient pas : c'est un UB. Donc, le compilateur peut partir du principe qu'on atteint jamais ce moment. En effet, si la fonction retourne 'true' avant d'atteindre l'UB, elle est correcte. LE compilateur peut donc remplacer toute la fonction par return true
.
On peut utiliser ceci à notre avantage :
Pour aller plus loin
Des effets assez spectaculaires peuvent se produire selon le contexte et l'UB. Par exemple, une fonction jamais appelée peut être appelée dans le code généré. On dit parfois que les UB peuvent remonter le temps, ou faire sortir des démons volants de vos narines (nasal demons).
Des fonctions peuvent avoir un UB selon leurs paramètres, comme sqrt(-1)
.
Last updated