web-dev-qa-db-fra.com

(Pourquoi) utilise un comportement non défini de variable non initialisée?

Si j'ai:

unsigned int x;
x -= x;

il est clair que x devrait être nul après cette expression, mais partout où je regarde, ils disent que le comportement de ce code n'est pas défini, pas simplement la valeur de x (jusqu'à avant la soustraction).

Deux questions:

  • Le comportement de ce code est-il vraiment indéfini?
    (Par exemple, le code pourrait-il se bloquer [ou pire] sur un système compatible?)

  • Si c'est le cas, pourquoi est-ce que C dit que le comportement n'est pas défini, alors qu'il est parfaitement clair que x doit être nul ici?

    c'est-à-dire quel est l'avantage donné en ne définissant pas le comportement ici?

De toute évidence, le compilateur pourrait simplement utiliser quelle que soit la valeur de déchets qu'il jugeait "pratique" à l'intérieur de la variable, et cela fonctionnerait comme prévu ... approche?

78
user541686

Oui, ce comportement n'est pas défini, mais pour des raisons différentes de celles que la plupart des gens connaissent.

Premièrement, l'utilisation d'une valeur unialisée n'est pas en soi un comportement indéfini, mais la valeur est simplement indéterminée. Accéder à cela est alors UB si la valeur se trouve être une représentation d'interruption pour le type. Les types non signés ont rarement des représentations d'interruption, vous seriez donc relativement en sécurité de ce côté.

Ce qui rend le comportement non défini est une propriété supplémentaire de votre variable, à savoir qu'elle "aurait pu être déclarée avec register", c'est-à-dire que son adresse n'est jamais prise. De telles variables sont traitées spécialement parce qu'il existe des architectures qui ont de vrais registres CPU qui ont une sorte d'état supplémentaire qui est "non initialisé" et qui ne correspond pas à une valeur dans le domaine de type.

Edit: La phrase pertinente de la norme est 6.3.2.1p2:

Si la valeur l désigne un objet de durée de stockage automatique qui aurait pu être déclaré avec la classe de stockage de registre (n'a jamais eu son adresse prise), et que cet objet n'est pas initialisé (non déclaré avec un initialiseur et aucune affectation à celui-ci n'a été effectuée avant utilisation) ), le comportement n'est pas défini.

Et pour être plus clair, le code suivant est légal en toutes circonstances:

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • Ici, les adresses de a et b sont prises, donc leur valeur est juste indéterminée.
  • Puisque unsigned char n'a jamais de représentation d'interruption selon laquelle une valeur indéterminée est simplement non spécifiée, toute valeur de unsigned char pourrait arriver.
  • À la fin, a doit contenir la valeur 0.

Edit2: a et b ont des valeurs non spécifiées:

3.19.3 valeur non spécifiée
valeur valide du type pertinent lorsque la présente Norme internationale n'impose aucune exigence quant à la valeur choisie dans tous les cas

82
Jens Gustedt

La norme C donne aux compilateurs une grande latitude pour effectuer des optimisations. Les conséquences de ces optimisations peuvent être surprenantes si vous supposez un modèle naïf de programmes où la mémoire non initialisée est définie sur un motif binaire aléatoire et toutes les opérations sont effectuées dans l'ordre où elles sont écrites.

Remarque: les exemples suivants ne sont valables que parce que x n'a jamais son adresse prise, elle est donc "semblable à un registre". Ils seraient également valides si le type de x avait des représentations d'interruption; c'est rarement le cas pour les types non signés (cela nécessite de "gaspiller" au moins un bit de stockage, et doit être documenté), et impossible pour unsigned char. Si x avait un type signé, alors l'implémentation pourrait définir le modèle de bits qui n'est pas un nombre entre 2n-1-1) et 2n-1-1 comme représentation piège. Voir --- (réponse de Jens Gustedt .

Les compilateurs tentent d'affecter des registres aux variables, car les registres sont plus rapides que la mémoire. Étant donné que le programme peut utiliser plus de variables que le processeur ne possède de registres, les compilateurs effectuent l'allocation des registres, ce qui conduit à différentes variables utilisant le même registre à des moments différents. Considérez le fragment de programme

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

Lorsque la ligne 3 est évaluée, x n'est pas encore initialisée, donc (pour des raisons de compilation) la ligne 3 doit être une sorte de coup de chance qui ne peut pas se produire en raison d'autres conditions que le compilateur n'était pas assez intelligent pour comprendre. en dehors. Puisque z n'est pas utilisé après la ligne 4 et x n'est pas utilisé avant la ligne 5, le même registre peut être utilisé pour les deux variables. Donc, ce petit programme est compilé pour les opérations suivantes sur les registres:

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

La valeur finale de x est la valeur finale de r0 Et la valeur finale de y est la valeur finale de r1. Ces valeurs sont x = -3 et y = -4, et non 5 et 4 comme cela se produirait si x avait été correctement initialisé.

Pour un exemple plus élaboré, considérez le fragment de code suivant:

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

Supposons que le compilateur détecte que condition n'a pas d'effet secondaire. Puisque condition ne modifie pas x, le compilateur sait que le premier passage dans la boucle ne peut pas accéder à x car il n'est pas encore initialisé. Par conséquent, la première exécution du corps de la boucle est équivalente à x = some_value(), il n'est pas nécessaire de tester la condition. Le compilateur peut compiler ce code comme si vous aviez écrit

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

La façon dont cela peut être modélisé à l'intérieur du compilateur est de considérer que toute valeur dépendant de x a quelle que soit la valeur qui convient tant que x n'est pas initialisé. Parce que le comportement lorsqu'une variable non initialisée n'est pas définie, plutôt que la variable ayant simplement une valeur non spécifiée, le compilateur n'a pas besoin de garder trace d'une relation mathématique spéciale entre les valeurs qui conviennent. Ainsi, le compilateur peut analyser le code ci-dessus de cette manière:

  • lors de la première itération de boucle, x n'est pas initialisé au moment où -x est évalué.
  • -x A un comportement indéfini, donc sa valeur est celle qui convient le mieux.
  • La règle d'optimisation condition ? value : value S'applique, ce code peut donc être simplifié en condition; value.

Lorsqu'il est confronté au code de votre question, ce même compilateur analyse que lorsque x = - x Est évalué, la valeur de -x Est celle qui convient. Ainsi, l'affectation peut être optimisée.

Je n'ai pas cherché d'exemple de compilateur qui se comporte comme décrit ci-dessus, mais c'est le genre d'optimisations que les bons compilateurs essaient de faire. Je ne serais pas surpris d'en rencontrer un. Voici un exemple moins plausible d'un compilateur avec lequel votre programme plante. (Ce n'est peut-être pas si invraisemblable si vous compilez votre programme dans une sorte de mode de débogage avancé.)

Ce compilateur hypothétique mappe chaque variable dans une page de mémoire différente et configure les attributs de page de sorte que la lecture à partir d'une variable non initialisée provoque une interruption du processeur qui appelle un débogueur. Toute affectation à une variable s'assure d'abord que sa page mémoire est mappée normalement. Ce compilateur n'essaie pas d'effectuer une optimisation avancée - il est dans un mode de débogage, destiné à localiser facilement les bogues tels que les variables non initialisées. Lorsque x = - x Est évalué, le côté droit provoque un déroutement et le débogueur se déclenche.

22
Gilles

Oui, le programme peut se bloquer. Il peut y avoir, par exemple, des représentations d'interruptions (modèles de bits spécifiques qui ne peuvent pas être traités) qui peuvent provoquer une interruption du processeur, qui non gérée pourrait planter le programme.

(6.2.6.1 sur un projet C11 tardif dit) Certaines représentations d'objets n'ont pas besoin de représenter une valeur du type d'objet. Si la valeur stockée d'un objet a une telle représentation et est lue par une expression lvalue qui n'a pas de type de caractère, le comportement n'est pas défini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou partie de l'objet par une expression lvalue qui n'a pas de type de caractère, le comportement n'est pas défini.50) Une telle représentation est appelée une représentation trap.

(Cette explication ne s'applique qu'aux plateformes où unsigned int peut avoir des représentations pièges, ce qui est rare sur les systèmes du monde réel; voir les commentaires pour les détails et les renvois à des causes alternatives et peut-être plus courantes qui conduisent au libellé actuel de la norme.)

16
eq-

(Cette réponse concerne C 1999. Pour C 2011, voir la réponse de Jens Gustedt.)

La norme C ne dit pas que l'utilisation de la valeur d'un objet de durée de stockage automatique non initialisée est un comportement indéfini. La norme C 1999 dit, en 6.7.8 10, "Si un objet qui a une durée de stockage automatique n'est pas initialisé explicitement, sa valeur est indéterminée." (Ce paragraphe définit ensuite comment les objets statiques sont initialisés, donc les seuls objets non initialisés qui nous préoccupent sont les objets automatiques.)

3.17.2 définit la "valeur indéterminée" comme "soit une valeur non spécifiée, soit une représentation d'interruption". 3.17.3 définit "valeur non spécifiée" comme "valeur valide du type pertinent où la présente Norme internationale n'impose aucune exigence quant à la valeur choisie dans tous les cas".

Ainsi, si unsigned int x Non initialisé a une valeur non spécifiée, alors x -= x Doit produire zéro. Reste à savoir s'il peut s'agir d'une représentation piège. L'accès à une valeur d'interruption provoque un comportement indéfini, selon 6.2.6.1 5.

Certains types d'objets peuvent avoir des représentations d'interruption, comme les signaux NaN de nombres à virgule flottante. Mais les entiers non signés sont spéciaux. Conformément au 6.2.6.2, chacun des N bits de valeur d'un entier non signé représente une puissance de 2, et chaque combinaison des bits de valeur représente l'une des valeurs de 0 à 2N-1. Ainsi, les entiers non signés peuvent avoir des représentations d'interruption uniquement en raison de certaines valeurs dans leurs bits de remplissage (comme un bit de parité).

Si, sur votre plate-forme cible, un int non signé n'a pas de bits de remplissage, un int non signé non initialisé ne peut pas avoir de représentation d'interruption et l'utilisation de sa valeur ne peut pas provoquer un comportement non défini.

13
Eric Postpischil

Oui, ce n'est pas défini. Le code peut se bloquer. C dit que le comportement n'est pas défini car il n'y a aucune raison spécifique de faire une exception à la règle générale. L'avantage est le même que tous les autres cas de comportement indéfini - le compilateur n'a pas à générer de code spécial pour que cela fonctionne.

De toute évidence, le compilateur pourrait simplement utiliser la valeur de déchets qu'il jugeait "pratique" à l'intérieur de la variable, et cela fonctionnerait comme prévu ... qu'est-ce qui ne va pas avec cette approche?

Pourquoi pensez-vous que cela ne se produit pas? C'est exactement l'approche adoptée. Le compilateur n'est pas requis pour le faire fonctionner, mais il n'est pas requis pour le faire échouer.

11
David Schwartz

Pour toute variable de tout type, qui n'est pas initialisée ou pour d'autres raisons contient une valeur indéterminée, ce qui suit s'applique pour le code lisant cette valeur:

  • Dans le cas où la variable a une durée de stockage automatique et n'a pas son adresse prise, le code invoque toujours un comportement indéfini [1] .
  • Sinon, dans le cas où le système prend en charge les représentations d'interruption pour le type de variable donné, le code invoque toujours un comportement indéfini [2].
  • Sinon, s'il n'y a pas de représentation d'interruption, la variable prend une valeur non spécifiée. Il n'y a aucune garantie que cette valeur non spécifiée soit cohérente à chaque lecture de la variable. Cependant, il est garanti de ne pas être une représentation piège et il est donc garanti de ne pas invoquer de comportement indéfini [3].

    La valeur peut ensuite être utilisée en toute sécurité sans provoquer de plantage du programme, bien que ce code ne soit pas portable pour les systèmes avec des représentations d'interruption.


[1]: C11 6.3.2.1:

Si la valeur l désigne un objet de durée de stockage automatique qui aurait pu être déclaré avec la classe de stockage de registre (jamais eu son adresse prise), et cet objet n'est pas initialisé (non déclaré avec un initialiseur et aucune affectation à celui-ci n'a été effectuée avant utilisation) ), le comportement n'est pas défini.

[2]: C11 6.2.6.1:

Certaines représentations d'objets n'ont pas besoin de représenter une valeur du type d'objet. Si la valeur stockée d'un objet a une telle représentation et est lue par une expression lvalue qui n'a pas de type de caractère, le comportement n'est pas défini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou partie de l'objet par une expression lvalue qui n'a pas de type de caractère, le comportement n'est pas défini.50) Une telle représentation est appelée une représentation trap.

[3] C11:

3.19.2
valeur indéterminée
soit une valeur non spécifiée, soit une représentation d'interruption

3.19.3
valeur non spécifiée
valeur valide du type pertinent lorsque la présente Norme internationale n'impose aucune exigence quant à la valeur choisie dans tous les cas
REMARQUE Une valeur non spécifiée ne peut pas être une représentation d'interruption.

3.19.4
représentation des pièges
une représentation d'objet qui n'a pas besoin de représenter une valeur du type d'objet

6
Lundin

Alors que de nombreuses réponses se concentrent sur les processeurs qui interceptent l'accès aux registres non initialisés, des comportements excentriques peuvent survenir même sur des plateformes qui n'ont pas de tels interruptions, en utilisant des compilateurs qui ne font aucun effort particulier pour exploiter UB. Considérez le code:

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

un compilateur pour une plate-forme comme le ARM où toutes les instructions autres que les chargements et les magasins opèrent sur des registres 32 bits peuvent raisonnablement traiter le code d'une manière équivalente à:

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

Si l'une ou l'autre des lectures volatiles donne une valeur non nulle, r0 sera chargé avec une valeur dans la plage 0 ... 65535. Sinon, elle donnera tout ce qu'elle contenait lors de l'appel de la fonction (c'est-à-dire la valeur passée en x), qui pourrait ne pas être une valeur dans la plage 0..65535. Le Standard n'a pas de terminologie pour décrire le comportement d'une valeur dont le type est uint16_t mais dont la valeur est en dehors de la plage de 0..65535, sauf pour dire que toute action qui pourrait produire un tel comportement invoque UB.

2
supercat