web-dev-qa-db-fra.com

Qu'advient-il d'une variable déclarée non initialisée en C? At-il une valeur?

Si en C j'écris:

int num;

Avant d’attribuer quoi que ce soit à num, la valeur de num est-elle indéterminée?

130
atp

Les variables statiques (portée du fichier et fonction statique) sont initialisées à zéro:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Les variables non statiques (variables locales) sont indéterminées . Leur lecture avant d'attribuer une valeur entraîne un comportement indéfini.

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

En pratique, ils ont tendance à ne contenir qu'une valeur insensée au début - certains compilateurs peuvent même indiquer des valeurs fixes spécifiques pour rendre évident le fait de regarder dans un débogueur. démons par les voies nasales .

En ce qui concerne le comportement indéfini au lieu de simplement "valeur indéfinie/arbitraire", un certain nombre d'architectures de CPU disposent de bits de drapeau supplémentaires dans leur représentation pour différents types. Un exemple moderne serait le Itanium, qui a un bit "Not a Thing" dans ses registres ; Bien entendu, les rédacteurs de la norme C envisageaient des architectures plus anciennes.

Tenter de travailler avec une valeur avec ces bits de drapeau définis peut entraîner une exception CPU dans une opération qui vraiment ne devrait pas échouer (par exemple, addition d'entier, ou assigner à une autre variable). Et si vous laissez une variable non initialisée, le compilateur risque de récupérer des erreurs aléatoires avec ces bits d'indicateur définis, ce qui signifie que toucher cette variable non initialisée peut s'avérer mortel.

177
bdonlan

0 si statique ou global, indéterminé si la classe de stockage est auto

C a toujours été très spécifique sur les valeurs initiales des objets. Si global ou static, ils seront mis à zéro. Si auto, la valeur est indéterminée.

C'était le cas dans les compilateurs antérieurs à C89, comme le spécifiaient K & R et le rapport C original de DMR.

C’était le cas dans C89, voir la section 6.5.7 Initialisation .

Si un objet ayant une durée de stockage automatique n'est pas initialisé explicitement, sa valeur est indéterminée. Si un objet dont la durée de stockage statique est définie n'est pas explicitement initialisé, il est implicitement initialisé comme si chaque membre possédant un type arithmétique était affecté de 0 et que chaque membre possédant un type de pointeur était affecté d'une constante de pointeur nulle.

C’était le cas dans C99, voir la section 6.7.8 Initialisation .

Si un objet ayant une durée de stockage automatique n'est pas initialisé explicitement, sa valeur est indéterminée. Si un objet ayant une durée de stockage statique n'est pas initialisé explicitement, alors:
- s'il est de type pointeur, il est initialisé à un pointeur nul;
- s'il est de type arithmétique, il est initialisé à zéro (positif ou non signé);
- s’il s’agit d’un agrégat, chaque membre est initialisé (récursivement) conformément à ces règles;
- s'il s'agit d'un syndicat, le premier membre nommé est initialisé (de manière récursive) conformément à ces règles.

Quant à ce que signifie exactement indéterminé, je ne suis pas sûr que pour C89, C99 dit:

3.17.2
valeur indéterminée

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

Mais quelles que soient les normes en vigueur, chaque page de pile commence réellement par zéro, mais lorsque votre programme examine toute valeur de classe de stockage auto, il voit tout ce qui a été laissé par votre propre programme Dernière utilisé ces adresses de pile. Si vous allouez beaucoup de tableaux auto, vous les verrez éventuellement commencer proprement par des zéros.

Vous pourriez vous demander, pourquoi est-ce ainsi? Une réponse différente SO traite cette question, voir: https://stackoverflow.com/a/2091505/14074

57
DigitalRoss

Cela dépend de la durée de stockage de la variable. Une variable ayant une durée de stockage statique est toujours implicitement initialisée à zéro.

Comme pour les variables automatiques (locales), une variable non initialisée a valeur indéterminée. Une valeur indéterminée, entre autres choses, signifie que quelle que soit la "valeur" que vous "voyez" dans cette variable, elle n'est pas seulement imprévisible, elle n'est même pas garantie d'être stable. Par exemple, en pratique (c'est-à-dire en ignorant l'UB pendant une seconde), ce code

int num;
int a = num;
int b = num;

ne garantit pas que les variables a et b recevront des valeurs identiques. Il est intéressant de noter que ce n’est pas un concept théorique pédant, cela se produit facilement en conséquence de l’optimisation.

Donc, en général, la réponse populaire selon laquelle "il est initialisé avec tout le contenu de la mémoire" n'est même pas correct à distance. Le comportement de la variable non initialisée est différent de celui d'une variable initialisée avec garbage.

11
AnT

Ubuntu 15.10, Kernel 4.2.0, x86-64, exemple GCC 5.2.1

Assez de normes, regardons une implémentation :-)

Variable locale

Normes: comportement indéfini.

Mise en œuvre: le programme alloue de l’espace de pile et ne déplace jamais rien à cette adresse. Par conséquent, tout ce qui se trouvait auparavant est utilisé.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

compiler avec:

gcc -O0 -std=c99 a.c

les sorties:

0

et décompile avec:

objdump -dr a.out

à:

0000000000400536 <main>:
  400536:       55                      Push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

De notre connaissance des conventions d’appel x86-64:

  • %rdi Est le premier argument printf, ainsi la chaîne "%d\n" À l'adresse 0x4005e4

  • %rsi Est le deuxième argument printf, donc i.

    Il provient de -0x4(%rbp), qui est la première variable locale à 4 octets.

    À ce stade, rbp se trouve sur la première page de la pile allouée par le noyau. Par conséquent, pour comprendre cette valeur, il convient d'examiner le code du noyau et de déterminer le paramètre défini.

    TODO Le noyau attribue-t-il quelque chose à cette mémoire avant de la réutiliser pour d'autres processus à la mort d'un processus? Sinon, le nouveau processus serait capable de lire la mémoire d'autres programmes terminés, en laissant des données en fuite. Voir: Les valeurs non initialisées représentent-elles un risque pour la sécurité?

Nous pouvons alors aussi jouer avec nos propres modifications de pile et écrire des choses amusantes comme:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Variables globales

Normes: 0

Implémentation: section .bss.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

compile pour:

0000000000400536 <main>:
  400536:       55                      Push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i> Indique que i est à l'adresse 0x601044 Et:

readelf -SW a.out

contient:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

qui dit que 0x601044 se trouve au milieu de la section .bss, qui commence à 0x601040 et fait 8 octets de long.

Le norme ELF garantit alors que la section nommée .bss Est complètement remplie de zéros:

.bss Cette section contient des données non initialisées qui contribuent à l’image mémoire du programme. Par définition, le système initialise les données avec des zéros lorsque le programme commence à s'exécuter. La section n'occupe aucun espace fichier, comme l'indique le type de section, SHT_NOBITS.

De plus, le type SHT_NOBITS Est efficace et n'occupe aucun espace sur le fichier exécutable:

sh_size Ce membre donne la taille en octets de la section. À moins que le type de section ne soit SHT_NOBITS, La section occupe sh_size Octets dans le fichier. Une section de type SHT_NOBITS Peut avoir une taille différente de zéro, mais elle n'occupe pas d'espace dans le fichier.

Ensuite, il appartient au noyau Linux de mettre à zéro cette région de mémoire lors du chargement du programme en mémoire lors de son démarrage.

Ça dépend. Si cette définition est globale (en dehors de toute fonction), alors num sera initialisé à zéro. Si c'est local (à l'intérieur d'une fonction) alors sa valeur est indéterminée. En théorie, même tenter de lire la valeur a un comportement indéfini - C permet la possibilité que des bits ne contribuent pas à la valeur, mais doivent être définis de manière spécifique pour que vous puissiez même obtenir des résultats définis en lisant la variable.

4
Jerry Coffin

La réponse de base est oui, c'est indéfini.

Si vous constatez un comportement étrange à cause de cela, cela peut dépendre de l'endroit où il est déclaré. Si vous vous trouvez dans une fonction de la pile, le contenu sera probablement différent à chaque appel de la fonction. S'il s'agit d'une portée statique ou modulaire, elle n'est pas définie mais ne changera pas.

1
simon

Si la classe de stockage est statique ou globale, lors du chargement, BSS initialise la variable ou l'emplacement mémoire (ML) sur 0 sauf si une valeur est initialement attribuée à la variable. Dans le cas de variables locales non initialisées, la représentation d'interruption est affectée à l'emplacement de la mémoire. Donc, si l'un de vos registres contenant des informations importantes est écrasé par le compilateur, le programme peut se bloquer.

mais certains compilateurs peuvent avoir un mécanisme pour éviter un tel problème.

Je travaillais avec la série n8 de la série v850 lorsque j’ai réalisé qu’il existe une représentation des interruptions qui comporte des modèles de bits qui représentent des valeurs non définies pour les types de données, à l’exception de char. Quand j'ai pris un caractère non initialisé, j'ai obtenu une valeur par défaut de zéro en raison de la représentation du piège. Cela pourrait être utile pour any1 utilisant necv850es

1
hanish

Étant donné que les ordinateurs ont une capacité de stockage limitée, les variables automatiques sont généralement contenues dans des éléments de stockage (registres ou RAM) précédemment utilisés à d'autres fins arbitraires. Si une telle variable est utilisée avant qu'une valeur ne lui ait été affectée, cette mémoire peut contenir tout ce qu'elle avait précédemment, de sorte que le contenu de la variable sera imprévisible.

De plus, de nombreux compilateurs peuvent conserver des variables dans des registres plus grands que les types associés. Bien qu'un compilateur soit requis pour garantir que toute valeur écrite dans une variable et relue sera tronquée et/ou étendue à la taille appropriée du signe, de nombreux compilateurs effectueront cette troncature lorsque des variables seront écrites et s'attendent à ce qu'elle ait été effectué avant la lecture de la variable. Sur de tels compilateurs, quelque chose comme:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

cela pourrait très bien avoir pour résultat que wow() stocke les valeurs 1234567 dans les registres 0 et 1, respectivement, et appelle foo(). Puisque x n'est pas nécessaire dans "foo" et que les fonctions sont supposées mettre leur valeur de retour dans le registre 0, le compilateur peut allouer le registre 0 à q. Si mode vaut 1 ou 3, le registre 0 sera chargé avec 2 ou 4 respectivement, mais s'il s'agit d'une autre valeur, la fonction peut renvoyer tout ce qui était dans le registre 0 (c'est-à-dire la valeur 1234567), même si la valeur n'est pas dans la plage de uint16_t.

Pour éviter de demander aux compilateurs de faire un travail supplémentaire pour s'assurer que les variables non initialisées ne semblent jamais contenir de valeurs en dehors de leur domaine et d'éviter de spécifier des comportements indéterminés avec des détails excessifs, la norme indique que l'utilisation de variables automatiques non initialisées est un comportement indéfini. Dans certains cas, les conséquences peuvent être encore plus surprenantes qu'une valeur hors de portée de ce type. Par exemple, étant donné:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

un compilateur pourrait en déduire qu'invoquer moo() avec un mode supérieur à 3 mènera inévitablement au programme appelant Undefined Behavior, le compilateur peut omettre tout code qui ne serait pertinent que si mode 4 ou plus, tel que le code qui empêcherait normalement le lancement d’armes nucléaires dans de tels cas. Notez que ni la philosophie standard ni celle du compilateur moderne ne tiennent compte du fait que la valeur renvoyée par "hey" est ignorée - le fait d'essayer de la renvoyer donne à un compilateur une licence illimitée pour générer du code arbitraire.

1
supercat