wake-up-neo.net

Durch das Dereferenzieren eines typpunzierten Zeigers werden die strengen Aliasing-Regeln verletzt

Ich habe den folgenden Code verwendet, um Daten aus Dateien als Teil eines größeren Programms zu lesen.

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;
    }

Jetzt wird mir gesagt, dass ich -O2 verwenden soll und erhalte folgende gcc-Warnung: warning: dereferencing type-punned pointer will break strict-aliasing rules

Bei Google habe ich zwei orthogonale Antworten gefunden:

vs

Am Ende möchte ich die Warnungen nicht ignorieren. Was würden Sie empfehlen?

[update] Ich habe das Spielzeugbeispiel durch die echte Funktion ersetzt.

45
Framester

Es sieht so aus, als würden Sie wirklich fread verwenden:

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

Das heißt, wenn Sie den Weg des Lesens von Zeichen gehen und diese dann als int interpretieren möchten, ist der sichere Weg in C (aber nicht in C++) die Verwendung einer Vereinigung:

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

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

Ich bin nicht sicher, warum die Länge von data in Ihrem ursprünglichen Code 3 ist. Ich gehe davon aus, dass Sie 4 Bytes wollten. Zumindest kenne ich keine Systeme, bei denen ein Int 3 Byte groß ist.

Beachten Sie, dass sowohl Ihr Code als auch mein Code nicht portierbar sind.

Bearbeiten: Wenn Sie Ints verschiedener Länge portabel aus einer Datei lesen möchten, versuchen Sie Folgendes:

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

(Hinweis: In einem echten Programm möchten Sie zusätzlich den Rückgabewert von fgetc () gegen EOF testen.)

Dies liest ein 4-Byte-Zeichen ohne Vorzeichen aus der Datei im Little-Endian-Format, , unabhängig von, was die Endianness des Systems ist. Es sollte auf fast jedem System funktionieren, bei dem ein unsigniertes Zeichen mindestens 4 Byte umfasst.

Wenn Sie endian-neutral sein möchten, verwenden Sie keine Zeiger oder Vereinigungen. Verwenden Sie stattdessen Bit-Shifts.

26
Martin B

Das Problem tritt auf, weil Sie über einen double* auf ein Char-Array zugreifen:

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

Gcc geht jedoch davon aus, dass Ihr Programm niemals über Zeiger anderen Typs auf Variablen zugreifen kann. Diese Annahme wird als striktes Aliasing bezeichnet und ermöglicht dem Compiler einige Optimierungen:

Wenn der Compiler weiß, dass Ihre *(double*) sich in keiner Weise mit data[] überschneiden kann, ist es allen möglichen Dingen gestattet, Ihren Code neu zu ordnen: 

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

Die Schleife wird höchstwahrscheinlich wegoptimiert und Sie erhalten nur noch:

return *(double*)data;

Dadurch bleiben Ihre Daten [] nicht initialisiert. In diesem speziellen Fall kann der Compiler möglicherweise sehen, dass sich die Zeiger überlappen. Wenn Sie jedoch char* data angegeben hätten, könnte dies zu Fehlern geführt haben.

Die strikte Aliasing-Regel besagt jedoch, dass ein Zeichen * und void * auf jeden Typ zeigen kann. So können Sie es neu schreiben in:

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

Strikte Aliasing-Warnungen sind wirklich wichtig, um sie zu verstehen oder zu beheben. Sie verursachen Fehler, die nicht intern reproduziert werden können, da sie nur bei einem bestimmten Compiler auf einem bestimmten Betriebssystem auf einer bestimmten Maschine und nur bei Vollmond und einmal im Jahr usw. auftreten.

39
Lasse Reinhold

Dieses Dokument fasst die Situation zusammen: http://dbp-consulting.com/tutorials/StrictAliasing.html

Es gibt verschiedene Lösungen, aber die tragbarste/sicherste ist die Verwendung von memcpy (). (Die Funktionsaufrufe werden möglicherweise optimiert, sodass sie nicht so ineffizient sind, wie sie erscheinen.) Ersetzen Sie beispielsweise Folgendes:

return *(short*)data;

Mit diesem:

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

Die Verwendung einer Vereinigung ist nicht das Richtige hier. Das Lesen von einem ungeschriebenen Mitglied der Union ist undefiniert - das heißt, der Compiler kann Optimierungen durchführen, die Ihren Code beschädigen (z. B. das Optimieren des Schreibvorgangs).

7
anon

Im Grunde können Sie die Nachricht von gcc lesen als Mann, den Sie nach Ärger suchen, sagen Sie nicht, ich hätte Sie nicht gewarnt .

Das Umwandeln eines Drei-Byte-Zeichen-Arrays in ein int ist eines der schlimmsten Dinge, die ich je gesehen habe. Normalerweise hat Ihr int mindestens 4 Bytes. Für das vierte (und vielleicht mehr, wenn int breiter ist) erhalten Sie zufällige Daten. Und dann gibst du das alles in eine double.

Mach einfach nichts davon. Das Aliasing-Problem, vor dem gcc warnt, ist unschuldig im Vergleich zu dem, was Sie tun. 

2
Jens Gustedt

Die Autoren des C-Standards wollten, dass Compiler-Writer effizienten Code generieren, wenn theoretisch möglich, aber unwahrscheinlich ist, dass der Zugriff auf eine globale Variable mit einem scheinbar nicht zusammenhängenden Zeiger erfolgt. Die Idee bestand nicht darin, das Typpunning zu verbieten, indem ein Zeiger in einem einzelnen Ausdruck geworfen und dereferenziert wird. Vielmehr sollte man das so sagen:

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

ein Compiler könnte davon ausgehen, dass das Schreiben in * d x nicht beeinflusst. Die Autoren des Standards wollten Situationen auflisten, in denen eine Funktion wie die oben genannte, die einen Zeiger aus einer unbekannten Quelle erhielt, davon ausgehen musste, dass sie einen scheinbar nicht zusammenhängenden Globus als Alias ​​bezeichnen könnte, ohne dass die Typen perfekt übereinstimmen. Zwar deutet die Begründung stark darauf hin, dass die Verfasser der Norm beabsichtigen, einen Standard für die Mindestkonformität in Fällen zu beschreiben, in denen ein Compiler ansonsten keinen Grund zu der Annahme hätte, dass es sich um Aliasnamen handeln könnte , dass die Compiler nicht Aliasing erkennen müssen in Fällen, in denen es offensichtlich ist und die Autoren von gcc haben entschieden, dass sie das kleinste Programm generieren, das sie mit der schlecht geschriebenen Sprache des Standards erstellen können, statt Code zu generieren, der eigentlich nützlich ist Um Aliasing in Fällen zu erkennen, in denen es offensichtlich ist (obwohl immer noch angenommen werden kann, dass Dinge nicht wie Aliasnamen aussehen oder nicht), fordern die Programmierer eher memcpy und benötigen daher einen Compiler Möglichkeit, dass Zeiger unbekannter Herkunft fast alles Alias ​​nennen, was die Optimierung behindert.

0
supercat