Qu'est-ce que Comportement indéfini (undefined behavior) ?

Comportement sur lequel la norme du C ou du C++ n'impose aucune exigence. En font partie le dépassement de capacité d'un entier signé, la lecture ou l'écriture hors d'un tableau, le déréférencement d'un pointeur NULL, l'utilisation d'une zone libérée et le décalage à gauche d'une valeur négative. Le compilateur peut optimiser en supposant qu'un programme correct n'en contient jamais : un tel code peut fonctionner comme prévu par hasard, ou donner des résultats inattendus, par exemple un test qui disparaît avec certaines options d'optimisation. Il se distingue du comportement défini par l'implémentation, que l'implémentation choisit et documente.

Nom japonais: 未定義動作

« Comportement indéfini (undefined behavior) » apparaît dans 10 questions d'entraînement réparties sur 2 thèmes, dont Bases de la programmation en C, Compilateurs.

Domaine: Bases de la programmation en C

Exemples typiques

On peut citer le dépassement de capacité dans un calcul sur des entiers signés (comme INT_MAX + 1), la lecture ou l'écriture hors d'un tableau, l'utilisation de la valeur d'une variable automatique non initialisée, la lecture ou l'écriture à travers un pointeur NULL ou un pointeur pendant, la double libération d'une même zone, la division d'un entier par 0, ainsi que le décalage d'un nombre de bits supérieur ou égal à la largeur du type et le décalage à gauche d'une valeur négative. Le dépassement d'un entier non signé n'est pas un comportement indéfini : il est défini comme revenant au reste de la division par une puissance de 2.

Lien avec l'optimisation du compilateur

Comme le compilateur peut supposer qu'aucun comportement indéfini ne se produit, il peut considérer x + 1 > x comme toujours vrai, ou supprimer un test p == NULL placé après une lecture de *p. Un programme qui fonctionnait comme prévu à -O0 peut ainsi cesser de fonctionner simplement en passant à -O2 ou en mettant à jour le compilateur. Ce n'est pas une erreur du compilateur : la cause est que le programme enfreint une hypothèse de la norme.

Détection et prévention

Avec gcc ou clang, l'option -fsanitize=undefined (UBSan) détecte à l'exécution le dépassement de capacité des entiers signés et les erreurs semblables, et -fsanitize=address (ASan) les accès hors limites et les utilisations après libération. Les avertissements de -Wall et -Wextra, les outils d'analyse statique et les règles de codage comme MISRA C aident aussi à réduire les comportements indéfinis.

S'entraîner : questions sur Comportement indéfini (undefined behavior)

Quelle description du comportement indéfini, au sens de la norme du langage C, est correcte ?

  1. AUn comportement sur lequel la norme n'impose rien ; le compilateur peut optimiser en supposant qu'il ne se produit jamais, et le résultat peut être inattendu
  2. BUn comportement dont chaque implémentation fixe le résultat, avec l'obligation de le documenter
  3. CUn comportement toujours détecté à l'exécution, qui affiche un message d'erreur et arrête le programme
Afficher la réponse et l'explication

Réponse : A. Un comportement sur lequel la norme n'impose rien ; le compilateur peut optimiser en supposant qu'il ne se produit jamais, et le résultat peut être inattendu

Un comportement indéfini est un comportement sur lequel la norme n'impose aucune exigence, comme le dépassement de capacité d'un entier signé, l'accès hors des bornes d'un tableau, le déréférencement d'un pointeur NULL ou l'utilisation après libération. Il peut arriver qu'il fonctionne comme prévu, mais comme le compilateur peut optimiser en supposant qu'il « ne se produit pas dans un programme correct », il suffit de changer les options d'optimisation ou la version du compilateur pour que le résultat change. En revanche, ce que l'implémentation choisit et doit documenter, comme la taille d'un int ou le fait que char soit signé ou non, s'appelle un comportement défini par l'implémentation. Avec gcc et clang, l'option -fsanitize=undefined détecte une partie des comportements indéfinis à l'exécution.

int et unsigned int font 32 bits. Laquelle des opérations suivantes est un comportement indéfini selon la norme du langage C ?

  1. AAjouter 1 à une variable unsigned int dont la valeur est le maximum, 4294967295
  2. BDécaler vers la droite de 1 bit, avec >>, une valeur int négative
  3. CAjouter 1 à une variable int dont la valeur est le maximum, 2147483647
Afficher la réponse et l'explication

Réponse : C. Ajouter 1 à une variable int dont la valeur est le maximum, 2147483647

Une opération sur des entiers signés dont le résultat sort de la plage représentable, comme 2147483647 + 1 en int, est un comportement indéfini. L'arithmétique des entiers non signés est définie comme revenant au reste de la division par 2³² : 4294967295 + 1 donne 0, et ce n'est pas un comportement indéfini. Le décalage à droite d'une valeur négative est un comportement défini par l'implémentation, qui choisit entre décalage arithmétique et décalage logique ; gcc effectue un décalage arithmétique. En revanche, le décalage à gauche d'une valeur négative est un comportement indéfini.

Questions fréquentes

Quelle différence y a-t-il entre comportement indéfini et comportement défini par l'implémentation ?

Un comportement défini par l'implémentation, comme la taille d'un int, le fait que char soit signé ou non, ou le résultat du décalage à droite d'une valeur négative, est un comportement pour lequel l'implémentation choisit une option et la documente. Un comportement indéfini est un comportement sur lequel la norme n'impose aucune exigence : le résultat peut varier d'un cas à l'autre, même avec la même implémentation. Un programme portable rend explicites les endroits où il s'appuie sur un comportement défini par l'implémentation, et évite tout comportement indéfini.

Un code au comportement indéfini qui fonctionne pose-t-il problème ?

Oui. Même s'il fonctionne par hasard comme prévu dans l'environnement actuel, un changement d'options d'optimisation, de version du compilateur ou du code environnant peut donner un autre résultat. Du point de vue de la sécurité aussi, des comportements indéfinis comme les écritures hors limites peuvent être exploités par des attaquants.

« Comportement indéfini (undefined behavior) » dans les explications

Questions mentionnant « Comportement indéfini (undefined behavior) » (10)

  1. En C, le dépassement de capacité d'un entier signé est un comportement indéfini. Sur cette base, quel code un compilateur optimisant la fonction ci-dessus peut-il générer ?
  2. Quelle description du comportement indéfini, au sens de la norme du langage C, est correcte ?
  3. int et unsigned int font 32 bits. Laquelle des opérations suivantes est un comportement indéfini selon la norme du langage C ?
  4. uint8_t est un type entier non signé de 8 bits. Quelle est la sortie obtenue en exécutant le code ci-dessus, qui incrémente x 10 fois ?
  5. int fait 32 bits (valeur maximale 2147483647). Quelle est la plus grande valeur de n pour laquelle la fonction fact(n) ci-dessus renvoie correctement la factorielle de n, sans dépassement de capacité ?
  6. Quelle affirmation sur le pointeur renvoyé par la fonction make ci-dessus est correcte ?
  7. En C, quelle est la raison la plus appropriée pour laquelle il est recommandé d'écrire p = NULL; juste après free(p); ?
  8. En C, quelle description de ce qui se passe lorsqu'un pointeur NULL est déréférencé (lecture de *p alors que p vaut NULL) est correcte ?
  9. Quelle est la sortie obtenue en exécutant le code ci-dessus, qui utilise la macro SQUARE définie par #define ?
  10. Lorsque la fonction ci-dessus est compilée avec gcc en activant l'optimisation, le test if (p == NULL) peut être supprimé. Quelle en est la raison ?

Termes associés

Pointeur Variable Tableau Décalage arithmétique Récursion (récursivité) Pile (stack) Pointeur pendant (dangling pointer) Allocation dynamique de mémoire Tas (heap) Défaut de page (page fault)

Retour au glossaire