web-dev-qa-db-fra.com

Le pointeur de déréférencement typé va enfreindre les règles de crénelage strict

J'ai utilisé le code suivant pour lire des données à partir de fichiers dans le cadre d'un programme plus volumineux.

double data_read(FILE *stream,int code) {
        char data[8];
        switch(code) {
        case 0x08:
            return (unsigned char)fgetc(stream);
        case 0x09:
            return (signed char)fgetc(stream);
        case 0x0b:
            data[1] = fgetc(stream);
            data[0] = fgetc(stream);
            return *(short*)data;
        case 0x0c:
            for(int i=3;i>=0;i--)
                data[i] = fgetc(stream);
            return *(int*)data;
        case 0x0d:
            for(int i=3;i>=0;i--)
                data[i] = fgetc(stream);
            return *(float*)data;
        case 0x0e:
            for(int i=7;i>=0;i--)
                data[i] = fgetc(stream);
            return *(double*)data;
        }
        die("data read failed");
        return 1;
    }

Maintenant on me dit d'utiliser -O2 et je reçois l'avertissement suivant de gcc: warning: dereferencing type-punned pointer will break strict-aliasing rules

Googleing j'ai trouvé deux réponses orthogonales:

contre

En fin de compte, je ne veux pas ignorer les avertissements. Que recommanderais-tu?

[update] J'ai remplacé l'exemple de jouet par la fonction réelle.

45
Framester

Cela ressemble beaucoup à si vous voulez vraiment utiliser fread:

int data;
fread(&data, sizeof(data), 1, stream);

Cela dit, si vous voulez emprunter la voie de lecture des caractères, puis les réinterpréter comme un entier, le moyen le plus sûr de le faire en C (mais pas en C++) consiste à utiliser une union:

union
{
    char theChars[4];
    int theInt;
} myunion;

for(int i=0; i<4; i++)
    myunion.theChars[i] = fgetc(stream);
return myunion.theInt;

Je ne sais pas pourquoi la longueur de data dans votre code d'origine est 3. Je suppose que vous vouliez 4 octets; au moins, je ne connais aucun système où un int est de 3 octets.

Notez que votre code et le mien sont hautement non portables.

Edit: Si vous voulez lire des entiers de différentes longueurs à partir d’un fichier, vous pouvez essayer quelque chose comme ceci:

unsigned result=0;
for(int i=0; i<4; i++)
    result = (result << 8) | fgetc(stream);

(Remarque: dans un programme réel, vous voudriez également tester la valeur de retour de fgetc () avec EOF.)

Ceci lit un fichier non signé de 4 octets dans le fichier au format little-endian, indépendamment de de la finalité du système. Cela devrait fonctionner sur presque tous les systèmes où un unsigned est au moins 4 octets.

Si vous voulez rester neutre, n'utilisez pas de pointeur ni de syndicat; utilisez plutôt des décalages en bits.

26
Martin B

Le problème se produit car vous accédez à un tableau de caractères à l'aide d'un double*:

char data[8];
...
return *(double*)data;

Mais gcc suppose que votre programme n’accédera jamais aux variables avec des pointeurs de types différents. Cette hypothèse s'appelle strict-aliasing et permet au compilateur de procéder à certaines optimisations:

Si le compilateur sait que votre *(double*) ne peut en aucun cas chevaucher le data[], il est autorisé à faire toutes sortes de choses, comme réorganiser votre code dans: 

return *(double*)data;
for(int i=7;i>=0;i--)
    data[i] = fgetc(stream);

La boucle est probablement optimisée et vous vous retrouvez avec juste:

return *(double*)data;

Ce qui laisse vos données [] non initialisées. Dans ce cas particulier, le compilateur pourra peut-être voir que vos pointeurs se chevauchent, mais si vous l'aviez déclaré char* data, il aurait pu générer des bogues.

Cependant, la règle d'aliasing stricte stipule qu'un caractère * et vide * peuvent pointer sur n'importe quel type. Donc, vous pouvez le réécrire dans:

double data;
...
*(((char*)&data) + i) = fgetc(stream);
...
return data;

Les avertissements de crénelage rigoureux sont vraiment importants à comprendre ou à corriger. Ils provoquent des types de bogues impossibles à reproduire en interne car ils ne surviennent que sur un compilateur particulier sur un système d'exploitation donné sur une machine particulière et uniquement à la pleine lune et une fois par an, etc.

39
Lasse Reinhold

Cette documentation résume la situation: http://dbp-consulting.com/tutorials/StrictAliasing.html

Il existe différentes solutions, mais la plus portable/sûre consiste à utiliser memcpy (). (Les appels de fonction peuvent être optimisés en sortie, donc ce n'est pas aussi inefficace qu'il y paraît.) Par exemple, remplacez ceci:

return *(short*)data;

Avec ça:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
7
Thatcher Ulrich

Utiliser un syndicat est pas la bonne chose à faire ici. La lecture d’un membre non écrit de l’union n’est pas définie - c’est-à-dire que le compilateur est libre d’effectuer des optimisations qui vont casser votre code (comme l’optimisation de l’écriture).

7
anon

En gros, vous pouvez lire le message de gcc en tant que le type que vous recherchez, ne dites pas que je ne vous ai pas prévenu .

Transformer un tableau de caractères sur trois octets en une variable int est l’une des pires choses que j’ai jamais vue. Normalement, votre int a au moins 4 octets. Donc pour la quatrième (et peut-être plus si int est plus large), vous obtenez des données aléatoires. Et ensuite vous jetez tout cela dans une variable double.

Ne faites rien de tout ça. Le problème d'aliasing que gcc met en garde est innocent comparé à ce que vous faites. 

2
Jens Gustedt

Les auteurs de la norme C voulaient laisser les rédacteurs du compilateur générer un code efficace dans des circonstances où il serait théoriquement possible mais peu probable qu'une valeur globale puisse accéder à sa valeur à l'aide d'un pointeur apparemment non lié. L’idée n’était pas d’interdire le dactylographie en convertissant et en déréférençant un pointeur dans une seule expression, mais plutôt de dire cela à partir de quelque chose comme:

int x;
int foo(double *d)
{
  x++;
  *d=1234;
  return x;
}

un compilateur serait en droit de supposer que l'écriture dans * d n'affectera pas x. Les auteurs de la norme ont souhaité répertorier les situations dans lesquelles une fonction telle que celle décrite ci-dessus recevant un pointeur provenant d'une source inconnue devrait supposer qu'elle pourrait aliaser un global apparemment sans rapport, sans exiger que les types correspondent parfaitement. Malheureusement, alors que le raisonnement suggère fortement que les auteurs de la norme avaient l'intention de décrire une norme de conformité minimale dans les cas où un compilateur n'aurait autrement aucune raison de croire que les choses pourraient alias , la règle n'exige pas que les compilateurs reconnaissent le pseudonyme. dans les cas où cela est évident et les auteurs de gcc ont décidé qu'ils préféraient générer le plus petit programme possible tout en se conformant au langage mal écrit de la norme, plutôt que de générer un code réellement utile. de reconnaître le crénelage dans les cas où cela est évident (tout en pouvant supposer que les choses qui ne ressemblent pas à un alias ne le seront pas, ne le feront pas), ils préfèrent demander aux programmeurs d'utiliser memcpy, ce qui nécessite un compilateur possibilité que des pointeurs d'origine inconnue puissent aliaser à peu près n'importe quoi, empêchant ainsi l'optimisation.

0
supercat