C Language
Comportement non défini
Recherche…
Introduction
En C, certaines expressions génèrent un comportement indéfini . Le standard choisit explicitement de ne pas définir comment un compilateur doit se comporter s'il rencontre une telle expression. En conséquence, un compilateur est libre de faire ce qu’il juge nécessaire et peut produire des résultats utiles, des résultats inattendus, voire un crash.
Le code qui appelle UB peut fonctionner comme prévu sur un système spécifique avec un compilateur spécifique, mais ne fonctionnera probablement pas sur un autre système, ou avec un compilateur, une version de compilateur ou des paramètres de compilateur différents.
Remarques
Qu'est-ce qu'un comportement indéfini (UB)?
Le comportement non défini est un terme utilisé dans la norme C. La norme C11 (ISO / IEC 9899: 2011) définit le terme comportement indéfini comme
comportement, lors de l'utilisation d'une structure de programme non portable ou erronée ou de données erronées, pour lesquels la présente Norme internationale n'impose aucune exigence
Que se passe-t-il s'il y a UB dans mon code?
Ce sont les résultats qui peuvent survenir en raison d’un comportement indéfini selon la norme:
NOTE Le comportement indéfini possible peut aller de l'ignorance complète de la situation à des résultats imprévisibles, au comportement lors de la traduction ou à l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans message de diagnostic), à la fin d'une traduction émission d'un message de diagnostic).
La citation suivante est souvent utilisée pour décrire (de manière moins formelle) des résultats provenant d'un comportement non défini:
"Lorsque le compilateur rencontre [une construction indéfinie donnée], il est légal de faire voler des démons" (l'implication est que le compilateur peut choisir n'importe quelle manière arbitraire d'interpréter le code sans violer la norme ANSI C)
Pourquoi UB existe-t-il?
Si c'est si mauvais, pourquoi ne l'ont-ils pas simplement défini ou défini?
Un comportement non défini offre davantage d'opportunités d'optimisation; Le compilateur peut légitimement supposer que tout code ne contient pas de comportement indéfini, ce qui lui permet d'éviter les vérifications à l'exécution et d'effectuer des optimisations dont la validité serait coûteuse ou impossible à prouver autrement.
Pourquoi UB est-il difficile à retrouver?
Il existe au moins deux raisons pour lesquelles un comportement non défini crée des bogues difficiles à détecter:
- Le compilateur n'est pas obligé de vous avertir - et ne peut généralement pas le faire de manière fiable - d'un comportement non défini. En fait, l'exiger de le faire irait directement à l'encontre de la raison d'être d'un comportement indéfini.
- Les résultats imprévisibles pourraient ne pas commencer à se dérouler au point exact de l’opération où se produit la construction dont le comportement n’est pas défini; Un comportement non défini entrave toute l'exécution et ses effets peuvent survenir à tout moment: pendant, après ou même avant la construction indéfinie.
Considérez dereference pointeur nul: le compilateur n'est pas obligé de diagnostiquer le déréférencement de pointeur nul, et même ne pourrait pas, car à l'exécution, tout pointeur passé dans une fonction ou dans une variable globale peut être nul. Et lorsque le déréférencement de pointeur nul se produit, la norme ne prescrit pas que le programme doive se bloquer. Au lieu de cela, le programme peut tomber en panne plus tôt, plus tard ou ne pas se bloquer du tout; Il pourrait même se comporter comme si le pointeur null indiquait un objet valide et se comporter complètement normalement, uniquement pour se bloquer dans d'autres circonstances.
Dans le cas de déréférencement de pointeur nul, le langage C diffère des langages gérés tels que Java ou C #, où le comportement du déréférencement de pointeur nul est défini : une exception est levée à l'heure exacte ( NullPointerException
en Java, NullReferenceException
en C #) ainsi, ceux venant de Java ou de C # peuvent croire de manière incorrecte que dans un tel cas, un programme C doit tomber en panne, avec ou sans émission d'un message de diagnostic .
Information additionnelle
Plusieurs situations de ce type doivent être clairement distinguées:
- Comportement explicitement indéfini, c'est-à-dire que le standard C vous indique explicitement que vous êtes hors limites.
- Comportement implicitement indéfini, où il n'y a tout simplement pas de texte dans la norme prévoyant un comportement pour la situation dans laquelle vous avez amené votre programme.
Rappelez-vous également que dans de nombreux endroits, le comportement de certaines constructions est délibérément indéfini par le standard C pour laisser la place aux développeurs de bibliothèques et de compilateurs de proposer leurs propres définitions. Un bon exemple est celui des signaux et des gestionnaires de signaux, où les extensions de C, telles que la norme du système d'exploitation POSIX, définissent des règles beaucoup plus élaborées. Dans de tels cas, il vous suffit de vérifier la documentation de votre plate-forme; la norme C ne peut rien vous dire.
Notez également que si un comportement indéfini se produit dans le programme, cela ne signifie pas que le seul point où un comportement non défini a eu lieu est problématique, mais un programme entier devient sans signification.
En raison de telles préoccupations, il est important (surtout que les compilateurs ne nous avertissent pas toujours sur UB) pour que la programmation de personnes en C soit au moins familière avec le genre de choses qui déclenchent un comportement indéfini.
Il convient de noter que certains outils (par exemple des outils d’analyse statique tels que PC-Lint) facilitent la détection d’un comportement indéfini, mais là encore, ils ne peuvent pas détecter toutes les occurrences d’un comportement indéfini.
Déréférencer un pointeur nul
Ceci est un exemple de déréférencement d'un pointeur NULL, provoquant un comportement indéfini.
int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */
Un standard NULL
est garanti par le standard C pour se comparer à tout pointeur sur un objet valide, et le déréférencement appelle un comportement non défini.
Modifier un objet plus d'une fois entre deux points de séquence
int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;
Un tel code conduit souvent à des spéculations sur la "valeur résultante" de i
. Plutôt que de spécifier un résultat, les normes C spécifient que l'évaluation d'une telle expression produit un comportement indéfini . Avant C2011, la norme formalisait ces règles en termes de points de séquence :
Entre le point de séquence précédent et suivant, un objet scalaire aura sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker.
(Norme C99, section 6.5, paragraphe 2)
Ce schéma s’est révélé un peu trop grossier, ce qui a eu pour conséquence que certaines expressions présentant un comportement indéfini par rapport à C99 ne devraient pas le faire. C2011 conserve les points de séquence, mais introduit une approche plus nuancée de ce domaine basée sur le séquençage et une relation appelée "séquence avant":
Si un effet secondaire sur un objet scalaire est non séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou à un calcul de valeur utilisant la valeur du même objet scalaire, le comportement est indéfini. S'il existe plusieurs ordres autorisés des sous-expressions d'une expression, le comportement n'est pas défini si un effet secondaire non séquencé se produit dans l'un des classements.
(Norme C2011, section 6.5, paragraphe 2)
Les détails complets de la relation "séquencée avant" sont trop longs pour être décrits ici, mais ils complètent les points de séquence plutôt que de les supplanter. Ils ont donc pour effet de définir le comportement de certaines évaluations dont le comportement était auparavant indéfini. En particulier, s'il existe un point de séquence entre deux évaluations, celle qui précède le point de séquence est "séquencée avant" celle qui suit.
L'exemple suivant présente un comportement bien défini:
int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */
L'exemple suivant a un comportement non défini:
int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */
Comme pour toute forme de comportement indéfini, l'observation du comportement réel d'évaluation des expressions qui ne respectent pas les règles de séquençage n'est pas informative, sauf dans un sens rétrospectif. La norme linguistique ne permet pas d’attendre que de telles observations soient prédictives, même en ce qui concerne le comportement futur du même programme.
Déclaration de retour manquante dans la fonction de retour de valeur
int foo(void) {
/* do stuff */
/* no return here */
}
int main(void) {
/* Trying to use the (not) returned value causes UB */
int value = foo();
return 0;
}
Lorsqu'une fonction est déclarée pour renvoyer une valeur, elle doit le faire sur tous les chemins de code possibles. Un comportement indéfini se produit dès que l'appelant (qui attend une valeur de retour) tente d'utiliser la valeur de retour 1 .
Notez que le comportement non défini ne se produit que si l'appelant tente d'utiliser / d'accéder à la valeur de la fonction. Par exemple,
int foo(void) {
/* do stuff */
/* no return here */
}
int main(void) {
/* The value (not) returned from foo() is unused. So, this program
* doesn't cause *undefined behaviour*. */
foo();
return 0;
}
La fonction main()
est une exception à cette règle dans la mesure où il est possible de la terminer sans instruction return car une valeur de retour supposée de 0
sera automatiquement utilisée dans ce cas 2 .
1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)
Si le} qui termine une fonction est atteint et que la valeur de l'appel de fonction est utilisée par l'appelant, le comportement est indéfini.
2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)
atteindre le} qui termine la fonction principale renvoie une valeur de 0.
Débordement d'entier signé
Selon le paragraphe 6.5 / 5 de C99 et C11, l'évaluation d'une expression produit un comportement indéfini si le résultat n'est pas une valeur représentable du type de l'expression. Pour les types arithmétiques, cela s'appelle un débordement . L'arithmétique entière non signée ne déborde pas car le paragraphe 6.2.5 / 9 s'applique, ce qui réduit le résultat non signé qui serait autrement hors de portée. Il n'y a pas de disposition analogue pour les types entiers signés , cependant; celles-ci peuvent déborder et produisent un comportement indéfini. Par exemple,
#include <limits.h> /* to get INT_MAX */
int main(void) {
int i = INT_MAX + 1; /* Overflow happens here */
return 0;
}
La plupart des instances de ce type de comportement indéfini sont plus difficiles à reconnaître ou à prédire. Le débordement peut en principe provenir de toute opération d'addition, de soustraction ou de multiplication sur des entiers signés (soumis aux conversions arithmétiques habituelles) où il n'y a pas de limites ou de relations efficaces entre les opérandes pour l'empêcher. Par exemple, cette fonction:
int square(int x) {
return x * x; /* overflows for some values of x */
}
est raisonnable, et il fait ce qu'il faut pour des valeurs d'argument assez petites, mais son comportement n'est pas défini pour des valeurs d'argument plus importantes. Vous ne pouvez pas juger de la seule fonction si les programmes qui l’appellent ont un comportement indéfini. Cela dépend des arguments qu'ils lui transmettent.
D'autre part, considérez cet exemple trivial d'arithmétique d'entiers signés à sécurité de débordement:
int zero(int x) {
return x - x; /* Cannot overflow */
}
La relation entre les opérandes de l'opérateur de soustraction assure que la soustraction ne déborde jamais. Ou considérez cet exemple un peu plus pratique:
int sizeDelta(FILE *f1, FILE *f2) {
int count1 = 0;
int count2 = 0;
while (fgetc(f1) != EOF) count1++; /* might overflow */
while (fgetc(f2) != EOF) count2++; /* might overflow */
return count1 - count2; /* provided no UB to this point, will not overflow */
}
Tant que les compteurs ne débordent pas individuellement, les opérandes de la soustraction finale seront tous deux non négatifs. Toutes les différences entre deux de ces valeurs sont représentables sous la forme int
.
Utilisation d'une variable non initialisée
int a;
printf("%d", a);
La variable a
est un int
avec une durée de stockage automatique. L'exemple de code ci-dessus tente d'imprimer la valeur d'une variable non initialisée ( a
n'a jamais été initialisé). Les variables automatiques qui ne sont pas initialisées ont des valeurs indéterminées; leur accès peut entraîner un comportement indéfini.
Remarque: Les variables avec stockage statique ou thread local, y compris les variables globales sans le mot-clé static
, sont initialisées à zéro ou à leur valeur initialisée. D'où ce qui suit est légal.
static int b;
printf("%d", b);
Une erreur très courante consiste à ne pas initialiser les variables qui servent de compteurs à 0. Vous leur ajoutez des valeurs, mais comme la valeur initiale est garbage, vous appellerez Undefined Behavior , comme dans la question Compilation on terminal et Avertissement. symboles étranges
Exemple:
#include <stdio.h>
int main(void) {
int i, counter;
for(i = 0; i < 10; ++i)
counter += i;
printf("%d\n", counter);
return 0;
}
Sortie:
C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
counter += i;
^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
int i, counter;
^
= 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812
Les règles ci-dessus s'appliquent également aux pointeurs. Par exemple, les résultats suivants entraînent un comportement indéfini
int main(void)
{
int *p;
p++; // Trying to increment an uninitialized pointer.
}
Notez que le code ci-dessus en lui-même peut ne pas provoquer une erreur ou une erreur de segmentation, mais essayer de déréférencer ce pointeur ultérieurement entraînerait un comportement indéfini.
Déréférencer un pointeur à variable au-delà de sa durée de vie
int* foo(int bar)
{
int baz = 6;
baz += bar;
return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage
* duration (local variables), thus the returned pointer is not valid! */
int main (void)
{
int* p;
p = foo(5); /* (2) this expression's behavior is undefined */
*p = *p - 6; /* (3) Undefined behaviour here */
return 0;
}
Certains compilateurs le soulignent utilement. Par exemple, gcc
avertit avec:
warning: function returns address of local variable [-Wreturn-local-addr]
et clang
avertit avec:
warning: address of stack memory associated with local variable 'baz' returned
[-Wreturn-stack-address]
pour le code ci-dessus. Mais les compilateurs peuvent ne pas être en mesure d’aider en code complexe.
(1) Le renvoi de la référence à la variable déclarée static
est défini comme comportement, car la variable n'est pas détruite après avoir quitté l'étendue actuelle.
(2) Conformément à la norme ISO / IEC 9899: 2011 6.2.4 §2, "La valeur d'un pointeur devient indéterminée lorsque l'objet sur lequel il pointe atteint la fin de sa durée de vie."
(3) Déréférencer le pointeur renvoyé par la fonction foo
est un comportement indéfini car la mémoire à laquelle il fait référence contient une valeur indéterminée.
Division par zéro
int x = 0;
int y = 5 / x; /* integer division */
ou
double x = 0.0;
double y = 5.0 / x; /* floating point division */
ou
int x = 0;
int y = 5 % x; /* modulo operation */
Pour la deuxième ligne de chaque exemple, où la valeur du deuxième opérande (x) est zéro, le comportement est indéfini.
Notez que la plupart des implémentations de calcul à virgule flottante suivront une norme (par exemple, IEEE 754), auquel cas les opérations comme diviser par zéro auront des résultats cohérents (par exemple, INFINITY
) même si la norme C indique que l'opération n'est pas définie.
Accéder à la mémoire au-delà du bloc attribué
Un pointeur sur un morceau de mémoire contenant n
éléments ne peut être déréférencé que s'il se trouve dans la memory
de memory
et dans la memory + (n - 1)
. Déréférencer un pointeur en dehors de cette plage entraîne un comportement indéfini. À titre d'exemple, considérons le code suivant:
int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */
La troisième ligne accède au 4ème élément d'un tableau de 3 éléments seulement, ce qui conduit à un comportement indéfini. De même, le comportement de la deuxième ligne du fragment de code suivant est également mal défini:
int array[3];
array[3] = 0;
Notez que le fait de pointer devant le dernier élément d'un tableau n'est pas un comportement indéfini ( beyond_array = array + 3
est bien défini ici), mais le déréférencement est ( *beyond_array
est un comportement indéfini). Cette règle s'applique également à la mémoire allouée dynamiquement (comme les tampons créés via malloc
).
Copie de mémoire superposée
Une grande variété de fonctions de bibliothèque standard ont parmi leurs effets la copie de séquences d'octets d'une région mémoire à une autre. La plupart de ces fonctions ont un comportement indéfini lorsque les régions source et de destination se chevauchent.
Par exemple, ceci ...
#include <string.h> /* for memcpy() */
char str[19] = "This is an example";
memcpy(str + 7, str, 10);
... tente de copier 10 octets lorsque les zones de mémoire source et de destination se chevauchent de trois octets. Pour visualiser:
overlapping area
|
_ _
| |
v v
T h i s i s a n e x a m p l e \0
^ ^
| |
| destination
|
source
En raison du chevauchement, le comportement résultant est indéfini.
memcpy()
, strcpy()
, strcat()
, sprintf()
et sscanf()
fonctions standard de la bibliothèque avec une limitation de ce type. La norme dit de ces fonctions et de plusieurs autres fonctions:
Si la copie a lieu entre des objets qui se chevauchent, le comportement est indéfini.
La fonction memmove()
est la principale exception à cette règle. Sa définition spécifie que la fonction se comporte comme si les données source étaient d'abord copiées dans un tampon temporaire, puis écrites dans l'adresse de destination. Il n'y a pas d'exception pour les régions source et de destination qui se chevauchent, pas plus que nécessaire, donc memmove()
a un comportement bien défini dans de tels cas.
La distinction reflète une efficacité vs. compromis de généralité. La copie telle que ces fonctions se produit généralement entre des régions de mémoire disjointes, et il est souvent possible de savoir, au moment du développement, si une instance particulière de copie de mémoire sera dans cette catégorie. En supposant que le non-chevauchement offre des implémentations comparativement plus efficaces qui ne produisent pas de résultats corrects de manière fiable lorsque l'hypothèse ne tient pas. La plupart des fonctions de la bibliothèque C sont autorisées pour les implémentations les plus efficaces, et memmove()
remplit les lacunes, servant les cas où la source et la destination peuvent ou se chevauchent. Pour produire l'effet correct dans tous les cas, cependant, il doit effectuer des tests supplémentaires et / ou utiliser une implémentation comparativement moins efficace.
Lecture d'un objet non initialisé qui n'est pas soutenu par la mémoire
La lecture d'un objet provoquera un comportement indéfini si l'objet est 1 :
- non initialisé
- défini avec une durée de stockage automatique
- son adresse n'est jamais prise
La variable a dans l'exemple ci-dessous satisfait toutes ces conditions:
void Function( void )
{
int a;
int b = a;
}
1 (Cité à partir de: ISO: CEI 9899: 201X 6.3.2.1 Lvalues, tableaux et indicateurs de fonction 2)
Si la lvalue désigne un objet de durée de stockage automatique qui aurait pu être déclaré avec la classe de stockage de registre (son adresse n'a jamais été prise) et cet objet n'est pas initialisé (non déclaré avec un initialiseur et aucune affectation n'a été effectuée avant son utilisation) ), le comportement est indéfini.
Course de données
C11 a introduit la prise en charge de plusieurs threads d'exécution, ce qui offre la possibilité de faire des courses de données. Un programme contient une course de données si un objet en y accède 1 par deux fils différents, où au moins l' un des accès est non-atomique, au moins on modifie l'objet, et la sémantique de programme ne parviennent pas à assurer que les deux accès ne peut pas chevaucher temporellement. 2 Notez bien que la concurrence réelle des accès impliqués n’est pas une condition pour une course de données; Les courses de données couvrent une classe plus large de problèmes liés aux incohérences (autorisées) dans les vues de la mémoire de différents threads.
Considérez cet exemple:
#include <threads.h>
int a = 0;
int Function( void* ignore )
{
a = 1;
return 0;
}
int main( void )
{
thrd_t id;
thrd_create( &id , Function , NULL );
int b = a;
thrd_join( id , NULL );
}
Les principaux appels fil thrd_create
pour démarrer une nouvelle fonction de défilement de fil Function
. Le deuxième thread modifie a
, et le thread principal lit a
. Aucun de ces accès n'est atomique, et les deux threads ne font rien individuellement ou conjointement pour s'assurer qu'ils ne se chevauchent pas, il y a donc une course aux données.
Parmi les façons dont ce programme pourrait éviter la course aux données:
- le thread principal pourrait effectuer sa lecture d'
a
avant de démarrer l'autre thread; - le fil conducteur peut effectuer sa lecture d'
a
après s'être assuré viathrd_join
que l'autre a pris fin; - les threads peuvent synchroniser leurs accès via un mutex, chacun verrouillant ce mutex avant d'accéder à
a
et de le déverrouiller ensuite.
Comme le démontre l'option mutex, éviter une course de données ne nécessite pas de garantir un ordre spécifique d'opérations, tel que le thread enfant modifiant a
avant que le thread principal ne le lise; il suffit (pour éviter une course de données) de s'assurer que pour une exécution donnée, un accès se produira avant l'autre.
1 Modifier ou lire un objet.
2 (Cité d'après l'ISO: CEI 9889: 201x, section 5.1.2.4 "Exécutions multithread et courses de données")
L'exécution d'un programme contient une course de données si elle contient deux actions en conflit dans des threads différents, dont au moins une n'est pas atomique et aucune ne se produit avant l'autre. Toute course de données de ce type entraîne un comportement indéfini.
Valeur de lecture du pointeur libéré
Même le simple fait de lire la valeur d'un pointeur qui a été libéré (c'est-à-dire sans essayer de déréférencer le pointeur) est un comportement non défini (UB), par exemple
char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{
}
Citant ISO / IEC 9899: 2011 , section 6.2.4 §2:
[…] La valeur d'un pointeur devient indéterminée lorsque l'objet vers lequel il pointe (ou juste passé) atteint la fin de sa vie.
L'utilisation d'une mémoire indéterminée pour n'importe quoi, y compris une comparaison ou une arithmétique apparemment sans danger, peut avoir un comportement indéfini si la valeur peut être une représentation de piège pour le type.
Modifier le littéral de chaîne
Dans cet exemple de code, le pointeur de caractère p
est initialisé à l'adresse d'un littéral de chaîne. Essayer de modifier le littéral de chaîne a un comportement indéfini.
char *p = "hello world";
p[0] = 'H'; // Undefined behavior
Cependant, modifier un tableau mutable de char
directement, ou par le biais d'un pointeur, n'est naturellement pas un comportement indéfini, même si son initialiseur est une chaîne littérale. Ce qui suit va bien:
char a[] = "hello, world";
char *p = a;
a[0] = 'H';
p[7] = 'W';
En effet, le littéral de chaîne est effectivement copié dans le tableau chaque fois que le tableau est initialisé (une fois pour les variables avec une durée statique, chaque fois que le tableau est créé pour les variables avec une durée automatique ou de thread). il est bon de modifier le contenu du tableau.
Libérer deux fois la mémoire
Libérer deux fois la mémoire est un comportement indéfini, par exemple
int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);
Citation de la norme (7.20.3.2. La fonction libre de C99):
Sinon, si l'argument ne correspond pas à un pointeur précédemment renvoyé par la fonction calloc, malloc ou realloc, ou si l'espace a été libéré par un appel à free ou realloc, le comportement n'est pas défini.
Utiliser un spécificateur de format incorrect dans printf
L'utilisation d'un spécificateur de format incorrect dans le premier argument de printf
appelle un comportement indéfini. Par exemple, le code ci-dessous appelle un comportement indéfini:
long z = 'B';
printf("%c\n", z);
Voici un autre exemple
printf("%f\n",0);
La ligne de code ci-dessus est un comportement indéfini. %f
attend le double. Cependant 0 est de type int
.
Notez que votre compilateur peut généralement vous aider à éviter de tels cas si vous activez les indicateurs appropriés lors de la compilation ( -Wformat
in clang
et gcc
). Du dernier exemple:
warning: format specifies type 'double' but the argument has type
'int' [-Wformat]
printf("%f\n",0);
~~ ^
%d
La conversion entre les types de pointeurs produit un résultat incorrectement aligné
Le comportement suivant peut être indéfini en raison d'un alignement de pointeur incorrect:
char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
uint32_t *intptr = (uint32_t*)(memory_block + 1); /* possible undefined behavior */
uint32_t mvalue = *intptr;
Le comportement indéfini se produit lorsque le pointeur est converti. Selon C11, si une conversion entre deux types de pointeurs produit un résultat incorrectement aligné (6.3.2.3), le comportement est indéfini . Ici, un uint32_t
pourrait nécessiter un alignement de 2 ou 4 par exemple.
calloc
d'autre part est requis pour retourner un pointeur qui est convenablement aligné pour tout type d'objet; Ainsi, memory_block
est correctement aligné pour contenir un uint32_t
dans sa partie initiale. Ensuite, sur un système où uint32_t
a besoin d'un alignement de 2 ou 4, memory_block + 1
sera une adresse impaire et donc mal alignée.
Observez que le standard C demande que l'opération de conversion soit déjà indéfinie. Ceci est imposé parce que sur les plates-formes où les adresses sont segmentées, l'adresse d'octet memory_block + 1
peut même ne pas avoir une représentation correcte en tant que pointeur entier.
Le fait de convertir char *
en pointeurs vers d'autres types sans se soucier des exigences d'alignement est parfois utilisé de manière incorrecte pour le décodage de structures empaquetées telles que les en-têtes de fichiers ou les paquets réseau.
Vous pouvez éviter le comportement indéfini résultant d'une conversion de pointeur mal alignée à l'aide de memcpy
:
memcpy(&mvalue, memory_block + 1, sizeof mvalue);
Ici, aucune conversion de pointeur en uint32_t*
n'a lieu et les octets sont copiés un par un.
Cette opération de copie pour notre exemple ne conduit qu'à une valeur valide de mvalue
car:
- Nous avons utilisé
calloc
, donc les octets sont correctement initialisés. Dans notre cas, tous les octets ont la valeur0
, mais toute autre initialisation correcte le ferait. -
uint32_t
est un type de largeur exacte et n'a pas de bits de remplissage - Tout modèle de bit arbitraire est une représentation valide pour tout type non signé.
Ajout ou soustraction de pointeur non borné correctement
Le code suivant a un comportement indéfini:
char buffer[6] = "hello";
char *ptr1 = buffer - 1; /* undefined behavior */
char *ptr2 = buffer + 5; /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6; /* OK, pointing to just beyond */
char *ptr4 = buffer + 7; /* undefined behavior */
Selon C11, si l'addition ou la soustraction d'un pointeur dans, ou juste au-delà, un objet tableau et un type entier produit un résultat qui ne pointe pas vers le même objet tableau ou juste au-delà, le comportement n'est pas défini (6.5.6 ).
De plus, il est naturellement indéfini de déréférencer un pointeur qui pointe juste au-delà du tableau:
char buffer[6] = "hello";
char *ptr3 = buffer + 6; /* OK, pointing to just beyond */
char value = *ptr3; /* undefined behavior */
Modification d'une variable const à l'aide d'un pointeur
int main (void)
{
const int foo_readonly = 10;
int *foo_ptr;
foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
*foo_ptr = 20; /* This is undefined behavior */
return 0;
}
Citant ISO / IEC 9899: 201x , section 6.7.3 §2:
Si vous tentez de modifier un objet défini avec un type qualifié en utilisant une lvalue avec un type non-qualifié, le comportement est indéfini. [...]
(1) Dans GCC, cela peut générer l'avertissement suivant: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
Passer un pointeur nul à la conversion de printf% s
La conversion %s
de printf
indique que l'argument correspondant est un pointeur sur l'élément initial d'un tableau de type caractère . Un pointeur nul ne pointe pas vers l'élément initial d'un tableau de type caractère, et le comportement des éléments suivants n'est donc pas défini:
char *foo = NULL;
printf("%s", foo); /* undefined behavior */
Cependant, le comportement indéfini ne signifie pas toujours que le programme se bloque - certains systèmes prennent des mesures pour éviter le plantage qui se produit normalement lorsqu'un pointeur nul est déréférencé. Par exemple, Glibc est connu pour imprimer
(null)
pour le code ci-dessus. Cependant, ajoutez (juste) une nouvelle ligne à la chaîne de format et vous obtiendrez un plantage:
char *foo = 0;
printf("%s\n", foo); /* undefined behavior */
Dans ce cas, cela se produit car GCC a une optimisation qui transforme printf("%s\n", argument);
dans un appel à puts
avec puts(argument)
, et puts
en glibc ne gère pas des pointeurs nuls. Tout ce comportement est conforme à la norme.
Notez que le pointeur null est différent d'une chaîne vide . Donc, ce qui suit est valide et n'a pas de comportement indéfini. Il ne vous reste plus qu'à imprimer une nouvelle ligne :
char *foo = "";
printf("%s\n", foo);
Liaison incohérente d'identificateurs
extern int var;
static int var; /* Undefined behaviour */
C11, §6.2.2, 7 dit:
Si, au sein d'une unité de traduction, le même identifiant apparaît avec une liaison interne et externe, le comportement est indéfini.
Notez que si une déclaration préalable d'un identifiant est visible, elle comportera le lien de la déclaration précédente. C11, §6.2.2, 4 le permet:
Pour un identi fi cateur déclaré avec le spéci fi cateur extern de classe de stockage dans une portée dans laquelle une déclaration préalable de cet identifiant est visible, 31) si la déclaration antérieure spéci fi e un lien interne ou externe, la liaison de l'identi fi cateur à la déclaration ultérieure est la même que le lien spécifié à la déclaration préalable. Si aucune déclaration préalable n'est visible ou si la déclaration préalable ne spécifie aucun lien, alors l'identi fi cateur a un lien externe.
/* 1. This is NOT undefined */
static int var;
extern int var;
/* 2. This is NOT undefined */
static int var;
static int var;
/* 3. This is NOT undefined */
extern int var;
extern int var;
Utiliser fflush sur un flux d'entrée
Les normes POSIX et C indiquent explicitement que l'utilisation de fflush
sur un flux d'entrée est un comportement non défini. La fflush
est définie uniquement pour les flux de sortie.
#include <stdio.h>
int main()
{
int i;
char input[4096];
scanf("%i", &i);
fflush(stdin); // <-- undefined behavior
gets(input);
return 0;
}
Il n'y a pas de méthode standard pour éliminer les caractères non lus d'un flux d'entrée. D'autre part, certaines implémentations utilisent fflush
pour effacer le tampon stdin
. Microsoft définit le comportement de fflush
sur un flux d'entrée: Si le flux est ouvert pour l'entrée, fflush
efface le contenu du tampon. Selon POSIX.1-2008, le comportement de fflush
est indéfini, à moins que le fichier d'entrée ne soit accessible.
Voir Utilisation de fflush(stdin)
pour plus de détails.
Déplacement de bits en utilisant des nombres négatifs ou au-delà de la largeur du type
Si la valeur du compte à rebours est une valeur négative, les opérations de décalage à gauche et à droite sont indéfinies 1 :
int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */
Si le décalage à gauche est effectué sur une valeur négative , il n'est pas défini:
int x = -5 << 3; /* undefined */
Si le décalage à gauche est effectué sur une valeur positive et que le résultat de la valeur mathématique n'est pas représentable dans le type, il n'est pas défini 1 :
/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit
* in an int. So, this is undefined. */
int x = 5 << 72;
Notez que le décalage vers la droite sur une valeur négative (.eg -5 >> 3
) n'est pas indéfini mais défini par la mise en œuvre .
1 Citant ISO / IEC 9899: 201x , section 6.5.7:
Si la valeur de l'opérande de droite est négative ou est supérieure ou égale à la largeur de l'opérande gauche promu, le comportement n'est pas défini.
Modification de la chaîne renvoyée par les fonctions getenv, strerror et setlocale
La modification des chaînes renvoyées par les fonctions standard getenv()
, strerror()
et setlocale()
est indéfinie. Ainsi, les implémentations peuvent utiliser un stockage statique pour ces chaînes.
La fonction getenv (), C11, § 7.22.4.7, 4 dit:
La fonction getenv renvoie un pointeur sur une chaîne associée au membre de liste correspondant. La chaîne pointée ne doit pas être modifiée par le programme, mais peut être écrasée par un appel ultérieur à la fonction getenv.
La fonction strerror (), C11, §7.23.6.3, 4 dit:
La fonction strerror renvoie un pointeur sur la chaîne dont le contenu est localpeci fi c. Le tableau désigné ne doit pas être modifié par le programme, mais peut être remplacé par un appel ultérieur à la fonction strerror.
La fonction setlocale (), C11, §7.11.1.1, 8 dit:
Le pointeur sur la chaîne renvoyé par la fonction setlocale est tel qu'un appel ultérieur avec cette valeur de chaîne et sa catégorie associée restaurera cette partie des paramètres régionaux du programme. La chaîne pointée ne doit pas être modifiée par le programme, mais peut être écrasée par un appel ultérieur à la fonction setlocale.
De même, la localeconv()
renvoie un pointeur sur struct lconv
qui ne doit pas être modifié.
La fonction localeconv (), C11, §7.11.2.1, 8 dit:
La fonction localeconv renvoie un pointeur sur l'objet rempli. La structure pointée par la valeur de retour ne doit pas être modifiée par le programme, mais peut être écrasée par un appel ultérieur à la fonction localeconv.
Retour d'une fonction déclarée avec le spécificateur de fonction `_Noreturn` ou` noreturn`
Le spécificateur de fonction _Noreturn
été introduit dans C11. L'en-tête <stdnoreturn.h>
fournit une macro noreturn
qui se développe en _Noreturn
. L'utilisation de _Noreturn
ou noreturn
partir de <stdnoreturn.h>
est donc <stdnoreturn.h>
et équivalente.
Une fonction déclarée avec _Noreturn
(ou noreturn
) n'est pas autorisée à retourner à son appelant. Si une telle fonction ne retourne à l'appelant, le comportement est indéfini.
Dans l'exemple suivant, func()
est déclaré avec le spécificateur noreturn
mais il renvoie à son appelant.
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void func(void);
void func(void)
{
printf("In func()...\n");
} /* Undefined behavior as func() returns */
int main(void)
{
func();
return 0;
}
gcc
et clang
produisent des avertissements pour le programme ci-dessus:
$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
}
^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^
Un exemple d'utilisation de noreturn
qui a un comportement bien défini:
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void my_exit(void);
/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
printf("Exiting...\n");
exit(0);
}
int main(void)
{
my_exit();
return 0;
}