Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Comportement indéfini ou pas ?

40 réponses
Avatar
David Côme
Bonjour à tous.
Est ce que ce code à un comportement indéfini ?

//iostream est inclue , ...
int a; //(1)
cout<< a; // (2)

Pour ma part:
Je pense que la valeur de a est indéfinie car (1) créer une variable sur
la pile sans lui affecter de valeur.
Elle prend donc la valeur de ce qui se trouvait avant à cette place.
Il n'y a pas de moyen de connaitre cette valeur. La valeure de a est
indéfini même si certain compilo réalisé une affectation par défaut
(je pense à VC++ en mode débug , on peut confirmer ?)

Par contre la 2eme ligne, elle a un comportement totalement défini.Elle va
afficher la valeur de a, qui peut être n'importe quoi.

Suis-je dans le vrai ?

Merci.

10 réponses

1 2 3 4
Avatar
Sylvain
James Kanze wrote on 28/02/2008 22:12:

À la mise sous tension, la mémoire contient effectivement
n'importe quoi. (Aussi, la plupart des contrôleurs dont je me
suis servi dans le temps prévoyaient bien une commande pour
écrire des valeurs erronées. Pour des motifs de test, si rien
d'autre. Mais le problème réel, c'est que jusqu'à la première
écriture, le contenu n'est pas défini, et il y a des chances
qu'il soit invalid.)


certes mais ce n'est pas le cas décrit ici.
évidemment toute la mémoire n'est pas vérifié à chaque tic d'horloge
(lorsqu'elle est rafraichie), elle l'est uniquement lorsqu'elle est lue,
or pour que le code: "int x; cout << x;" soit lu en mémoire et fourni au
CPU, il faut qu'il ait été écrit dans cette mémoire, lors de cette
écriture la valeur int32 (pas 33, ni 36, ni autre) non initialisée (en
supposant que le compilo n'est pas utilisé un registre) est écrite en
mémoire et son CRC est correctement calculé.

j'aurais même penser qu'une telle situation ne pouvait pas
durer plus de quelques nanosecondes (temps de "refresh"
moyen), à moins que la mémoire "qui n'a jamais été écrite" ne
soit pas rafraichie...


Dans le cas où je l'ai rencontré, il s'agissait des mémoires
statiques, sans refraiche. Mais je conçois bien aussi des cas où
des mémoires dynamiques ne font pas jouer la détection d'erreurs
lors des cycles de refraiche.


yep, à la lecture uniquement.

même dans ce cas je n'arrive pas à imaginer une valeur
spéciale faisant traper l'injecteur.


Qu'est-ce qui se passe s'il y a une erreur de parité, alors ?


en ECC ? le controleur l'a déjà corrigé.


Il ne peut pas en corriger toutes les erreurs possibles. Que des
erreurs d'un seul bit, et certaines de deux bits.


en effet, mais ici encore l'amalgame est erroné.

le chargement de la valeur non prédictible a copié tels-quels tous les
bits et a calculé le ou les bits de contrôle, si une particule cosmique
vient basculer un ou 2 bits, le CRC le(s) corrigera.

il n'a pas à corriger "toutes les valeurs possibles" d'une variable non
initialisée car cela ne veux strictement rien donne - elle est. elle n'a
pas beson de correction, n'étant pas erronée.

avec bit de parité simple, ça dépends du BIOS, mais cela ne
regarde nullement le code - avec variables non initialisées -
chargé; lorsqu'il est chargé, tous ces octets ont une parité
correcte ou le système (le hard!) était déjà défaillant.


Tu parles comme si on chargeait la mémoire à partir d'un disque.
(Dans un système classique, évidemment, il n'y a pas de chances
que ça se produisent, parce que le système écrit toute la
mémoire avant de te la donner.) Moi, j'ai rencontré le problème
dans des systèmes embarqués, avec le programme en PROM, et pas
de disque.


oui je me plaçais en effet dans ce cas qui me semblait pas trop
anecdotique, un contrôleur [E[E]]PROM utilise toujours un contrôle de
parité (en premier lieu pour se défendre comme les attaques) mais ici
encore il ne détecte que les modifications ayant pu intervenir de
manière ciblée (attaque lumière) ou globale (micro-ondes) sur la PROM.
il ne se demande pas si un int8 particulier contient tel valeur parce
que c'est le bruit du compilo ou parce que le source contenait une
valeur intentionelle.

en résumé, estimer qu'il peut exister des compilos suffisamment malin
pour insérer un trap (une assertion) lorsqu'une variable non initialisée
est utilisée - tout en étant assez bête pour ne pas en faire une erreur
de compilo - est recevable. (un compilo C51 - embarqué - n'insère pas de
tel code).

dire qu'une variable n bits non initialisée peut faire traper le système
parce qu'elle contient une valeur n+m bits qu'elle ne peut pas
contenir est une erreur.

Sylvain.




Avatar
Sylvain
Sylvain wrote on 28/02/2008 22:58:

il n'a pas à corriger "toutes les valeurs possibles" d'une variable non
initialisée car cela ne veux strictement rien donne - elle est. elle n'a
pas beson de correction, n'étant pas erronée.


cela ne veux strictement rien dire - elle est (ce qu'elle est).
elle n'a pas besoin de correction, n'étant pas erronée.

Sylvain - sans CRC lors de la frappe.

Avatar
James Kanze
On Feb 28, 10:58 pm, Sylvain wrote:
James Kanze wrote on 28/02/2008 22:12:

À la mise sous tension, la mémoire contient effectivement
n'importe quoi. (Aussi, la plupart des contrôleurs dont je me
suis servi dans le temps prévoyaient bien une commande pour
écrire des valeurs erronées. Pour des motifs de test, si rien
d'autre. Mais le problème réel, c'est que jusqu'à la première
écriture, le contenu n'est pas défini, et il y a des chances
qu'il soit invalid.)


certes mais ce n'est pas le cas décrit ici.


Bien sûr que si.

évidemment toute la mémoire n'est pas vérifié à chaque tic
d'horloge (lorsqu'elle est rafraichie), elle l'est uniquement
lorsqu'elle est lue, or pour que le code: "int x; cout << x;"
soit lu en mémoire et fourni au CPU, il faut qu'il ait été
écrit dans cette mémoire,


Où ça ?

Certes, il faut que x soit écrit avant d'être lu. Sinon,
il y a un comportement indéfini, avec potentiellement un crash
du système. Dans le bout du code présenté, et c'était le but de
de la question, il n'y a pas eu écriture de x. Il y a donc
comportement indéfini, et potentiellement un crash du système
CQFD.

lors de cette écriture la valeur int32 (pas 33, ni 36, ni
autre) non initialisée (en supposant que le compilo n'est pas
utilisé un registre) est écrite en mémoire et son CRC est
correctement calculé.


Quelle écriture. Justement, dans le cas en question, le mot
mémoire où se trouve x n'a jamais été écrit.

[...]
le chargement de la valeur non prédictible a copié tels-quels
tous les bits et a calculé le ou les bits de contrôle, si une
particule cosmique vient basculer un ou 2 bits, le CRC le(s)
corrigera.


Certes, mais ce dont on parle ici, ce n'est pas un bit qui a
changé, suite à n'importe quel aléa, mais la situation où tous
les bits du mot ont un contenu aléatoire, suite à la mise sous
tension.

il n'a pas à corriger "toutes les valeurs possibles" d'une
variable non initialisée car cela ne veux strictement rien
donne - elle est. elle n'a pas beson de correction, n'étant
pas erronée.


Sauf que la valeur leu sera bien erronée. Si on prend le cas
d'un mémoire »16 bits«, l'ECC en ajoute 5. À la mise sous
tension, ces 21 bits peuvent prendre 2097152 valeurs différent,
dont seulement 65536 sont valides. Si tu lis un mot mémoire
avant qu'il n'ait été écrit, tu n'as qu'une chance sur 32 que
l'ECC dit que c'est valide. Ensuite, l'ECC essaie de
corriger -- selon les cas, il y arrive (et tu vois une valeur
aléatoire) ou il n'y arrive pas (trop de bits erronés, ou
plutôt, une valeur qui ne pourrait se présenter que s'il y avait
trop de bits erronés). S'il n'y arrive pas, il provoque l'arrêt
du système, ou que sais-je.

Dans le cas de parité simple, évidemment, tu as une chance sur
deux que les 17 bits soit valides, il n'y a aucun essai de
correction, et donc, une chance sur deux que le contrôleur de la
mémoire provoque l'arrêt du système.

avec bit de parité simple, ça dépends du BIOS, mais cela ne
regarde nullement le code - avec variables non initialisées -
chargé; lorsqu'il est chargé, tous ces octets ont une parité
correcte ou le système (le hard!) était déjà défaillant.


Tu parles comme si on chargeait la mémoire à partir d'un disque.
(Dans un système classique, évidemment, il n'y a pas de chances
que ça se produisent, parce que le système écrit toute la
mémoire avant de te la donner.) Moi, j'ai rencontré le problème
dans des systèmes embarqués, avec le programme en PROM, et pas
de disque.


oui je me plaçais en effet dans ce cas qui me semblait pas trop
anecdotique, un contrôleur [E[E]]PROM utilise toujours un contrôle de
parité (en premier lieu pour se défendre comme les attaques) mais ici
encore il ne détecte que les modifications ayant pu intervenir de
manière ciblée (attaque lumière) ou globale (micro-ondes) sur la PRO M.
il ne se demande pas si un int8 particulier contient tel valeur parce
que c'est le bruit du compilo ou parce que le source contenait une
valeur intentionelle.

en résumé, estimer qu'il peut exister des compilos suffisamment malin
pour insérer un trap (une assertion) lorsqu'une variable non initialis ée
est utilisée - tout en étant assez bête pour ne pas en faire une err eur
de compilo - est recevable. (un compilo C51 - embarqué - n'insère pas de
tel code).

dire qu'une variable n bits non initialisée peut faire traper le systè me
parce qu'elle contient une valeur n+m bits qu'elle ne peut pas
contenir est une erreur.


Tu prétends que c'est impossible ? Est-ce que tu peux te
mettre dans la tête que je parle ici d'expérience. J'ai
réelement vu le cas. C'était bien avant le C++ -- je
programmais encore en assembleur à l'époque -- mais lire un mot
qui n'avait jamais été écrit provoquait bien le plantage du
système. (Avant de venir aux systèmes Unix, fin des années
1980, j'ai surtout travaillé sur des très petits systèmes
embarqués, à base de microprocesseurs, et sans disques. Souvent,
d'ailleurs, je concevais et le hard et le soft. J'ai déjà eu à
débogguer des contrôleurs de mémoire. Je sais donc de quoi je
parle.)

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
Marc Boyer
On 2008-02-27, Sylvain wrote:
Anthony Fleury wrote on 27/02/2008 19:29:

Est ce que ce code à un comportement indéfini ?

//iostream est inclue , ...
int a; //(1)
cout<< a; // (2)

Entre autre, a peut très bien contenir une valeur "invalide" pour le

système ("trap value") qui ferait remarquer au-dit système que la
variable utilisée ne l'est pas d'une manière conforme.


j'ai du mal à saisir la finesse de l'indéfinition.
je ne vois ici qu'une imprévision (non connaissance d'une valeur
aléatoire, ou comment se paraphraser).

surtout je ne vois pas ce que serait un *int* "invalide" !!
vous avez des machines sur lesquelles un int, disons 32 bits, peut
contenir toutes les valuers entières entre 0x00000000 et 0xFFFFFFFF
*plus* d'autres valeurs invalides ???


Il y a là un problème de vocabulaire qui peut durer longtemps...
La norme parle de "comportement indéfini" pour tout ce à quoi
elle ne veut pas associer de sémantique.
Après, sur l'ensemble des compilateurs existants à ce jour et
l'ensemble de OS et l'ensemble des processeurs et des mémoires,
ce sera surement un comportement "défini", avec une définition
plus ou moins précise quand même...

Après, l'exemple d'une exécution sous purify est intéressante.
En quoi le triplet gcc/linux/purify ne serait pas une "plateforme
d'exécution" et VC++/Vista le serait il ? Parce qu'aucun code
en production ne l'utilise ? En est-on sûr ?

Ca me rappelle les discutions sur
int i= 2;
int j = ++i * i++;
Les valeurs de i et j sont formellement indéfinies. Dans la
pratique, on imagine bien que i va avoir une valeur entre 3 et 4,
et j entre 4, 6, 9 et 16.

Sauf qu'en plus, la norme C (et donc par extension celle de C++)
évoque bien la notion de "trap representation", qu'on peut évoquer
là à cause de la non-initialisation.

Marc Boyer
--
Si tu peux supporter d'entendre tes paroles
Travesties par des gueux pour exciter des sots
IF -- Rudyard Kipling (Trad. André Maurois)



Avatar
Fabien LE LEZ
On Fri, 29 Feb 2008 09:20:19 +0000 (UTC), Marc Boyer :

Ca me rappelle les discutions sur
int i= 2;
int j = ++i * i++;
Les valeurs de i et j sont formellement indéfinies. Dans la
pratique, on imagine bien que i va avoir une valeur entre 3 et 4,
et j entre 4, 6, 9 et 16.


Quid du code ci-dessous ?

int Pre (int& n)
{
++n;
return n;
}

int Post (int& n)
{
++n;
return n-1;
}

int Mult (int a, int b)
{
return a*b;
}

int i= 2;
int j= Mult (Pre(i), Post(i));


Il me semble que dans ce cas, il n'y a que deux possibilités :

1ère possibilité :
1. Pre() est exécutée en entier -> i vaut 3 ; le premier temporaire
vaut 3
2. Post() est exécutée en entier -> i vaut 4 ; le deuxième temporaire
vaut 3
3. Mult() est exécutée en entier -> les deux paramètres sont les deux
temporaires ci-dessus ; résultat : 9

j == 9 ; i == 4

2e possibilité :
Post() est exécutée en entier -> i vaut 3 ; le deuxième temporaire
vaut 2
Pre() est exécutée en entier -> i vaut 4 ; le premier temporaire vaut
4
Mult() est exécutée en entier -> les deux paramètres sont les deux
temporaires ci-dessus ; résultat : 8

j == 8 ; i == 4


La norme assure-t-elle qu'on obtient un de ces deux résultats (sans
préciser lequel), ou bien est-ce un "comportement indéfini" dans toute
sa splendeur, i.e. un crash est-il théoriquement possible ?

Avatar
Jean-Marc Bourguet
Fabien LE LEZ writes:

On Fri, 29 Feb 2008 09:20:19 +0000 (UTC), Marc Boyer :

Ca me rappelle les discutions sur
int i= 2;
int j = ++i * i++;
Les valeurs de i et j sont formellement indéfinies. Dans la
pratique, on imagine bien que i va avoir une valeur entre 3 et 4,
et j entre 4, 6, 9 et 16.


Quid du code ci-dessous ?


Comportement non specifie (et pas indefini). J'ai pas verifie les details
de ton analyse, mais le principe est bon.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Marc Boyer
On 2008-02-29, Fabien LE LEZ wrote:
On Fri, 29 Feb 2008 09:20:19 +0000 (UTC), Marc Boyer :
1ère possibilité :
1. Pre() est exécutée en entier -> i vaut 3 ; le premier temporaire
vaut 3
2. Post() est exécutée en entier -> i vaut 4 ; le deuxième temporaire
vaut 3


Tu fais l'hypothèse que l'appel à la fonction est fait "en entier".
On sens bien que ce doit être vrai, mais difficile à trouver
explicitement. Le mieux que je trouve est 6.5.2.2/10 de la norme C
qui dit
"The order of evaluation of the function designator, the actual
arguments and subexpressions within the actual arguments is
unspecified", ce qui milite pour le fait qu'il y ait un ordre
et pas du parallélisme... Et comme l'appel de la fonction
et les ; qui sont dedans sont des points de séquence, en effet,
ton raisonnement doit être le bon.

Marc Boyer
--
Si tu peux supporter d'entendre tes paroles
Travesties par des gueux pour exciter des sots
IF -- Rudyard Kipling (Trad. André Maurois)

Avatar
Olivier Miakinen

même dans ce cas je n'arrive pas à imaginer une valeur spéciale faisant
traper l'injecteur.


As-tu lu le premier article de Jean-Marc Bourguet dans ce fil ?

<cit.>
il y a eu une machine n'ayant que des nombres en virgule flottante, mais
où certaines opérations trappaient si une donnée n'était pas entière
</cit.>

Avatar
James Kanze
On Feb 29, 10:20 am, Marc Boyer
wrote:
On 2008-02-27, Sylvain wrote:

Anthony Fleury wrote on 27/02/2008 19:29:

Est ce que ce code à un comportement indéfini ?

//iostream est inclue , ...
int a; //(1)
cout<< a; // (2)


Entre autre, a peut très bien contenir une valeur "invalide" pour le
système ("trap value") qui ferait remarquer au-dit système que la
variable utilisée ne l'est pas d'une manière conforme.


j'ai du mal à saisir la finesse de l'indéfinition. je ne
vois ici qu'une imprévision (non connaissance d'une valeur
aléatoire, ou comment se paraphraser).

surtout je ne vois pas ce que serait un *int* "invalide" !!
vous avez des machines sur lesquelles un int, disons 32
bits, peut contenir toutes les valuers entières entre
0x00000000 et 0xFFFFFFFF *plus* d'autres valeurs invalides
???


Il y a là un problème de vocabulaire qui peut durer longtemps...
La norme parle de "comportement indéfini" pour tout ce à quoi
elle ne veut pas associer de sémantique.
Après, sur l'ensemble des compilateurs existants à ce jour et
l'ensemble de OS et l'ensemble des processeurs et des mémoires,
ce sera surement un comportement "défini", avec une définition
plus ou moins précise quand même...


Oui et non. Lorsqu'il y a des comportements inféfinis, il peut
bien y avoir des aléas dans la pratique. Prenons un cas
concret : CP/M ou MS-DOS (quand tu compiles en modèle large).
Écrire à travers un pointeur non initialisé peut bien écrire
n'importe où, de façon plus ou moins aléatoire. (En fait, il
dépend de ce que tu as fait avant -- ce que la commande
précédante a laissé à l'adresse du pointeur. Mais de point de
vue pratique, je crois qu'on peut dire aléatoire.) C-à-d que si
tu n'as vraiment pas de chance, tu modifies le système---la
prochaine lecture disque devient une écriture, par exemple, ou
prèsque n'importe quoi d'autre. C'est vraiment un comportement
indéfini. (Si tu n'as vraiment pas de bol, il va falloir
réformatter la disquette.)

Après, l'exemple d'une exécution sous purify est intéressante.
En quoi le triplet gcc/linux/purify ne serait pas une "plateforme
d'exécution" et VC++/Vista le serait il ? Parce qu'aucun code
en production ne l'utilise ? En est-on sûr ?

Ca me rappelle les discutions sur
int i= 2;
int j = ++i * i++;
Les valeurs de i et j sont formellement indéfinies. Dans la
pratique, on imagine bien que i va avoir une valeur entre 3 et 4,
et j entre 4, 6, 9 et 16.


À l'époque, il a bien été expliqué qu'il existait des machines
où ce code pourrait provoquer un plantage. Je ne me rappelle
plus les détails, et j'avoue ne jamais avoir vu une telle
machine, mais l'explication tournait sur le fait que le
compilateur, sachant que les deux écritures ne pouvaient pas
référencier le même mot, pouvait générer des écritures en
parallel, et que selon ce qu'on disait, ça pourrait provoquer un
plantage sur certains systèmes. (J'ai bien travaillé dans des
contextes où il fallait tenir compte des temps d'accès dans les
instructions, sinon, plantage, mais d'une part, il s'agissait du
microcode -- il y aurait jamais du C++ là -- et de l'autre,
entre le lancement d'une lecture et la récupération de sa
valeur, on n'avait pas de droit de faire quoique ce soit avec la
mémoire. Même adresse ou pas.)

Sauf qu'en plus, la norme C (et donc par extension celle de C++)
évoque bien la notion de "trap representation", qu'on peut évoquer
là à cause de la non-initialisation.


Comme j'ai déjà dit ailleurs, j'ai réelement vu le cas où lire
de la mémoire qu'on n'avait jamais écrite pourrait provoquer un
plantage. Mais dans ce cas-là, il suffisait d'y avoir écrit une
fois, depuis la mise sous tension du système. En revanche, il y
avait bien les anciens processeurs de Burroughs/Unisys, qui
avait une architecture tagguée. En particulier, un entier
n'était qu'un virgule flottant avec le champs exposant à 0
(c-à-d 48 bits, avec 8 bits obligatoirement à 0, et magnitude
signée, en plus). Il n'y avait qu'une seule instruction add, par
exemple, qui fonctionnait à la fois pour les flottants et les
entiers, selon les valeurs qu'on lui donnait. Je ne sais pas ce
qui passait si tu mettais des bits d'un flottant dans une
variable de type entier en C, mais ça m'étonnerait que ce soit
quelque chose de bien prévisible.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34




Avatar
James Kanze
On Feb 29, 12:08 pm, Marc Boyer
wrote:
On 2008-02-29, Fabien LE LEZ wrote:

On Fri, 29 Feb 2008 09:20:19 +0000 (UTC), Marc Boyer :
1ère possibilité :
1. Pre() est exécutée en entier -> i vaut 3 ; le premier temporaire
vaut 3
2. Post() est exécutée en entier -> i vaut 4 ; le deuxième tempora ire
vaut 3


Tu fais l'hypothèse que l'appel à la fonction est fait "en entier".
On sens bien que ce doit être vrai, mais difficile à trouver
explicitement. Le mieux que je trouve est 6.5.2.2/10 de la norme C
qui dit
"The order of evaluation of the function designator, the actual
arguments and subexpressions within the actual arguments is
unspecified", ce qui milite pour le fait qu'il y ait un ordre
et pas du parallélisme... Et comme l'appel de la fonction
et les ; qui sont dedans sont des points de séquence, en effet,
ton raisonnement doit être le bon.


Il me semble qu'il y a eu un DR ou une démande de clarification
à cet égard adressé au comité C, et qu'ils ont bien dit que oui,
une fois qu'on est entré dans une fonction, on ne peut rien
faire d'autre avant d'avoir fini la fonction. C'est difficile à
comprendre ce que pourrait signifier le fait que l'appel et le
rétour d'une fonction soit des points de séquencement autrement.

En fait, la norme C (comme la norme C++ jusqu'aujourd'hui)
suppose un modèle séquentiel. Je dirais que ce n'est pas
tellement qu'on interdit l'exécution parallel des fonctions,
mais que la machine abstraite est séquentielle, et que le
résultat réel doit correspondre à une exécution possible de la
machine abstraite tant que le code est « correct » (pas de
comportement indéfini). Dans l'exemple de Fabien, la machine
abstraite peut appeler Pre avant Post, ou Post avant Pre, mais
il ne peut aller au délà que se elle peut prouver que le
résultat serait le même qu'un de ces deux cas, parce qu'il y a
bien des points de séquencement entre toutes les écritures.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


1 2 3 4