Une implémentation C ++ qui détecte un comportement indéfini?

Un grand nombre d’opérations en C ++ entraînent un comportement indéfini, où la spécification est complètement muette sur le comportement du programme et permet à tout ce qui se passe. De ce fait, il y a toutes sortes de cas où les gens ont du code qui comstack en mode debug mais pas en mode release, ou qui fonctionne jusqu’à ce qu’une modification apparemment sans rapport soit faite, ou fonctionne sur une machine mais pas sur une autre, etc.

Ma question est de savoir s’il existe un utilitaire qui examine l’exécution du code C ++ et marque toutes les instances où le programme appelle un comportement non défini. Bien que nous ayons des outils comme valgrind et les implémentations STL vérifiées, celles-ci ne sont pas aussi puissantes que ce que je pensais – valgrind peut avoir de faux négatifs si vous videz de la mémoire, par exemple, et vérifiez les implémentations STL ne va pas intercepter la suppression via un pointeur de classe de base.

Cet outil existe-t-il? Ou serait-il même utile de le faire traîner du tout?

EDIT : Je suis conscient qu’en général, il est indécis de vérifier statiquement si un programme C ++ peut exécuter quelque chose qui a un comportement indéfini. Cependant, il est possible de déterminer si une exécution spécifique d’un comportement C ++ produit un comportement indéfini. Une façon de faire serait de faire un interpréteur C ++ qui parcourt le code selon les définitions définies dans la spécification, à chaque point déterminant si le code a un comportement indéfini ou non. Cela ne détectera pas un comportement indéfini qui ne se produit pas lors de l’exécution d’un programme particulier, mais il détectera tout comportement indéfini qui se manifeste réellement dans le programme. Ceci est lié à la manière dont Turing-reconnaissable permet de déterminer si une MT accepte certaines entrées, même si elle est toujours indécidable en général.

Merci!

C’est une excellente question, mais laissez-moi vous donner une idée de la raison pour laquelle je pense que cela pourrait être impossible (ou du moins très difficile) en général.

Vraisemblablement, une telle implémentation serait presque un interpréteur C ++, ou au moins un compilateur pour quelque chose de plus comme Lisp ou Java. Il faudrait conserver des données supplémentaires pour chaque pointeur afin de vous assurer que vous n’avez pas effectué d’arithmétique en dehors d’un tableau ou que vous avez déréférencé quelque chose qui était déjà libéré ou autre.

Considérons maintenant le code suivant:

 int *p = new int; delete p; int *q = new int; if (p == q) *p = 17; 

Est-ce que le comportement *p = 17 indéfini? D’une part, il déréférencera p après sa libération. Par contre, le déréférencement q est correct et p == q

Mais ce n’est pas vraiment le but. Le fait est que le fait que le paramètre if évalué à true dépend des détails de l’implémentation du segment de mémoire, qui peuvent varier d’une implémentation à l’autre. Remplacez donc *p = 17 par un comportement indéfini, et vous avez un programme qui pourrait très bien exploser sur un compilateur normal, mais qui fonctionne correctement sur votre hypothétique “détecteur UB”. (Une implémentation C ++ typique utilisera une liste libre LIFO, de sorte que les pointeurs ont de bonnes chances d’être égaux. Un “détecteur UB” hypothétique pourrait fonctionner davantage comme un langage récupéré afin de détecter des problèmes après utilisation.)

En d’autres termes, l’existence d’un simple comportement défini par la mise en œuvre rend impossible, à mon avis, l’écriture d’un “détecteur UB” qui fonctionne pour tous les programmes.

Cela dit, un projet de création d’un “compilateur C ++ uber-ssortingct” serait très intéressant. Faites-moi savoir si vous voulez en commencer un. 🙂

John Regehr dans Finding Undefined Behavior Bugs par Finding Dead Code souligne un outil appelé STACK et que je cite à partir du site (c’est moi qui souligne ):

Le code d’optimisation-instable (code instable pour faire court) est une classe émergente de bogues logiciels: code éliminé de manière inattendue par les optimisations du compilateur en raison du comportement indéfini du programme. Le code instable est présent dans de nombreux systèmes, y compris le kernel Linux et le serveur de firebase database Postgres. Les conséquences du code instable vont des fonctionnalités incorrectes aux contrôles de sécurité manquants.

STACK est un vérificateur statique qui détecte le code instable dans les programmes C / C ++ . L’application de STACK à des systèmes largement utilisés a permis de découvrir 160 nouveaux bogues confirmés et corrigés par les développeurs.

En C ++ 11, dans le cas des variables constexpr et des fonctions, le comportement non défini doit être détecté au moment de la compilation .

Nous avons aussi gcc ubsan :

Récemment, GCC (version 4.9) a obtenu Undefined Behavior Sanitizer (ubsan), un vérificateur d’exécution pour les langages C et C ++. Pour vérifier votre programme avec ubsan, comstackz et liez le programme avec l’option -fsanitize = undefined. De tels binarys instrumentés doivent être exécutés; Si ubsan détecte un problème, il affiche un message d’erreur d’erreur d’exécution et, dans la plupart des cas, continue d’exécuter le programme.

et Clang Static Analyzer qui inclut de nombreux contrôles pour le comportement non défini. Par exemple, clangs -fsanitize contrôles qui inclut -fsanitize=undefined :

-fsanitize = undefined: vérificateur de comportement non défini rapide et compatible. Active les contrôles de comportement non définis qui ont un faible coût d’exécution et n’ont aucun impact sur la disposition de l’espace adresse ou ABI. Cela inclut toutes les vérifications répertoriées ci-dessous, à l’exception du non-sign-integer-overflow.

et pour C, nous pouvons examiner son article Il est temps de se pencher sérieusement sur l’exploitation de comportements indéfinis:

[..] J’avoue ne pas avoir personnellement le courage nécessaire pour cramer GCC ou LLVM via les meilleurs vérificateurs de comportement indéfinis dynamics disponibles: KCC et Frama-C . […]

Voici un lien vers kcc et je cite:

[…] Si vous essayez d’exécuter un programme qui n’est pas défini (ou pour lequel nous manquons de sémantique), le programme sera bloqué. Le message devrait vous dire où il est resté bloqué et peut donner un indice quant à pourquoi. Si vous voulez de l’aide pour déchiffrer le résultat, ou pour vous aider à comprendre pourquoi le programme n’est pas défini, envoyez-nous votre fichier .kdump. […]

et voici un lien vers Frama-C , un article où la première utilisation de Frama-C en tant qu’interprète C est décrite et un addendum à l’article.

Utiliser g++

 -Wall -Werror -pedantic-error 

(de préférence avec un argument -std approprié) -std pas mal de cas d’UB


Choses que -Wall vous permet d’inclure:

-pédant
Émettre tous les avertissements exigés par les normes ssortingctes ISO C et ISO C ++; rejette tous les programmes qui utilisent des extensions interdites, et certains autres programmes qui ne suivent pas les normes ISO C et ISO C ++. Pour ISO C, suivez la version de la norme ISO C spécifiée par toute option -std utilisée.

-Winit-self (C, C ++, Objective-C et Objective-C ++ uniquement)
Avertir à propos des variables non initialisées qui sont initialisées avec elles-mêmes. Notez que cette option ne peut être utilisée qu’avec l’option -Wuninitialized, qui à son tour ne fonctionne qu’avec -O1 et au-dessus.

-Wuninitialisé
Avertir si une variable automatique est utilisée sans être initialisée au préalable ou si une variable peut être bloquée par un appel “setjmp”.

et diverses choses interdites que vous pouvez faire avec les spécificateurs pour les fonctions de la famille printf et scanf .

Clang a une suite de désinfectants qui attrapent diverses formes de comportement indéfini. Leur objective final est d’être capable d’attraper tous les comportements indéfinis du langage de base C ++, mais il n’existe pas encore de vérifications pour certaines formes complexes de comportement indéfini.

Pour un ensemble décent d’assainissants, essayez:

 clang++ -fsanitize=undefined,address 

-fsanitize=address vérifie l’utilisation de mauvais pointeurs (ne pointant pas sur de la mémoire valide) et -fsanitize=undefined active un ensemble de contrôles UB légers (débordement d’entier, décalages incorrects, pointeurs mal alignés, …).

-fsanitize=memory (pour détecter les lectures de mémoire non initialisées) et -fsanitize=thread (pour détecter les courses de données) sont également utiles, mais ni l’un ni l’autre ne peuvent être combinés avec -fsanitize=address ni l’un avec l’autre l’espace d’adressage du programme.

Vous pourriez vouloir lire à propos de SAFECode .

Ceci est un projet de recherche de l’Université de l’Illinois, l’objective est indiqué sur la première page (liée ci-dessus):

Le but du projet SAFECode est de permettre la sécurité du programme sans la récupération de la mémoire et avec des contrôles d’exécution minimaux en utilisant une parsing statique lorsque cela est possible et des vérifications au moment de l’exécution si nécessaire. SAFECode définit une représentation de code avec des ressortingctions sémantiques minimales conçues pour permettre une application statique de la sécurité, en utilisant des techniques de compilateur agressives développées dans ce projet.

Ce qui m’intéresse vraiment, c’est l’élimination des vérifications d’exécution lorsque le programme peut être prouvé de façon statique, par exemple:

 int array[N]; for (i = 0; i != N; ++i) { array[i] = 0; } 

Ne devrait pas encourir plus de frais généraux que la version régulière.

Clang a des garanties sur le comportement indéfini, autant que je m’en souvienne, mais je ne peux pas mettre la main dessus …

Le compilateur clang peut détecter certains comportements non définis et les mettre en garde. Probablement pas aussi complet que vous voulez, mais c’est certainement un bon début.

Malheureusement, je ne suis pas au courant d’un tel outil. Typiquement, UB est défini comme tel précisément parce qu’il serait difficile ou impossible pour un compilateur de le diagnostiquer dans tous les cas.

En fait, votre meilleur outil est probablement les avertissements du compilateur: ils avertissent souvent des éléments de type UB (par exemple, destructeur non virtuel dans les classes de base, abus des règles de aliasing ssortingct, etc.).

La révision du code peut également aider à détecter les cas où UB est utilisé.

Ensuite, vous devez compter sur valgrind pour capturer les cas restants.

Juste comme une observation de côté, selon la théorie de la calculabilité, vous ne pouvez pas avoir un programme qui détecte tous les comportements non définis possibles.

Vous ne pouvez avoir que des outils qui utilisent des heuristiques et qui détectent certains cas particuliers qui suivent certains modèles. Ou vous pouvez dans certains cas prouver qu’un programme se comporte comme vous le souhaitez. Mais vous ne pouvez pas détecter un comportement indéfini en général.

modifier

Si un programme ne se termine pas (se bloque, se boucle pour toujours) sur une entrée donnée, sa sortie est indéfinie.

Si vous êtes d’accord sur cette définition, alors le fameux “problème d’interruption”, qui s’est révélé indécidable, c’est-à-dire qu’il n’existe aucun programme (Turing Machine, programme C, programme C ++, programme Pascal, etc.) quelle que soit la langue) qui peut résoudre ce problème en général.

Autrement dit: il n’existe pas de programme P pouvant prendre en entrée un programme Q et des données d’entrée I et imprimer en tant que sortie TRUE si Q (I) se termine ou bien imprimer FALSE si Q (I) ne se termine pas.

Pour plus d’informations, vous pouvez consulter http://en.wikipedia.org/wiki/Halting_problem .

Le comportement non défini n’est pas défini . Le mieux que vous puissiez faire est de vous conformer à la norme de manière pédagogique, comme d’autres l’ont suggéré, cependant, vous ne pouvez pas tester ce qui n’est pas défini, car vous ne savez pas ce que c’est. Si vous saviez ce que c’était et que les normes le spécifiaient, cela ne serait pas indéfini.

Cependant, si pour une raison quelconque, vous ne vous appuyez pas sur ce que la norme dit n’est pas défini , et que vous obtenez un résultat particulier, vous pouvez choisir de le définir et écrire des tests unitaires pour confirmer défini. Il est cependant préférable d’éviter tout comportement indéfini dans la mesure du possible.

Jetez un oeil à PCLint, c’est assez décent de détecter beaucoup de mauvaises choses en C ++.

Voici un sous-ensemble de ce qu’il attrape