Dereferencing type-punned pointer will break strict-aliasing rules


Utilicé el siguiente fragmento de código para leer datos de archivos como parte de un programa más grande.

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

Ahora me dicen que use -O2 y recibo la siguiente advertencia de gcc: warning: dereferencing type-punned pointer will break strict-aliasing rules

Googleing Encontré dos respuestas ortogonales:

Vs

Al final no quiero ignorar las advertencias. ¿Qué recomendarías?

[update] Sustituí el ejemplo toy por la función real.

Author: Community, 2010-07-14

7 answers

Parece que realmente quieres usar fread:

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

Dicho esto, si quieres seguir la ruta de leer caracteres, reinterpretándolos como un int, la forma segura de hacerlo en C (pero no en C++) es usar una unión:

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

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

No estoy seguro de por qué la longitud de data en su código original es 3. Asumo que querías 4 bytes; al menos no conozco ningún sistema donde un int es de 3 bytes.

Tenga en cuenta que tanto su código como el mío son altamente no portátil.

Edit: Si quieres leer enteros de varias longitudes de un archivo, portable, intentar algo como esto:

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

(Nota: En un programa real, adicionalmente querrías probar el valor devuelto de fgetc() contra EOF.)

Esto lee un 4-byte sin signo del archivo en formato little-endian, independientemente de de lo que es la endianidad del sistema. Debería funcionar en casi cualquier sistema donde un sin signo tenga al menos 4 bytes.

Si quieres ser endian-neutral, no use punteros o uniones; use cambios de bits en su lugar.

 26
Author: Martin B,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-24 09:46:16

El problema se produce porque se accede a un char-array a través de un double*:

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

Pero gcc asume que su programa nunca accederá a variables a través de punteros de tipo diferente. Esta suposición se llama strict-aliasing y permite al compilador hacer algunas optimizaciones:

Si el compilador sabe que su *(double*) no puede de ninguna manera superponerse con data[], está permitido todo tipo de cosas como reordenar su código en:

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

Lo más probable es que el bucle esté optimizado y terminas con solo:

return *(double*)data;

Lo que deja sus datos [] sin inicializar. En este caso particular, el compilador podría ser capaz de ver que sus punteros se superponen, pero si lo hubiera declarado char* data, podría haber dado errores.

Pero, la regla strict-aliasing dice que un char* y void* pueden apuntar a cualquier tipo. Así que puedes reescribirlo en:

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

Las advertencias estrictas de aliasing son realmente importantes de entender o corregir. Causan los tipos de errores que son imposibles de reproducir internamente porque ocurren solo en un compilador en particular en un sistema operativo en particular en una máquina en particular y solo en luna llena y una vez al año, etc.

 39
Author: Lasse Reinhold,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-28 17:53:12

Usar una unión es no lo correcto aquí. La lectura de un miembro no escrito de la unión no está definida, es decir, el compilador es libre de realizar optimizaciones que romperán su código(como optimizar la escritura).

 7
Author: anon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-22 18:31:52

Este documento resume la situación: http://dbp-consulting.com/tutorials/StrictAliasing.html

Hay varias soluciones diferentes, pero la más portátil/segura es usar memcpy(). (Las llamadas a funciones pueden estar optimizadas, por lo que no es tan ineficiente como parece.) Por ejemplo, reemplace esto:

return *(short*)data;

Con esto:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
 6
Author: Thatcher Ulrich,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-28 16:42:59

Básicamente puedes leer el mensaje de gcc como chico que estás buscando problemas, no digas que no te advertí .

Lanzar una matriz de caracteres de tres bytes a un int es una de las peores cosas que he visto, nunca. Normalmente su int tiene al menos 4 bytes. Así que para el cuarto (y tal vez más si int es más amplio) obtienes datos aleatorios. Y luego lanzas todo esto a un double.

Simplemente no hagas nada de eso. El problema de aliasing del que gcc advierte es inocente comparado con lo que eres hacer.

 2
Author: Jens Gustedt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-14 14:11:57

Los autores del Estándar C querían permitir a los escritores del compilador generar código eficiente en circunstancias donde sería teóricamente posible pero poco probable que una variable global pudiera tener su valor accedido usando un puntero aparentemente no relacionado. La idea no era prohibir el juego de tipo lanzando y desreferenciando un puntero en una sola expresión, sino más bien decir que dado algo como:

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

Un compilador tendría derecho a asumir que la escritura en *d no afectará a x. Los autores del Estándar querían enumerar situaciones en las que una función como la anterior que recibió un puntero de una fuente desconocida tendría que asumir que podría alias un global aparentemente no relacionado, sin requerir que los tipos coincidan perfectamente. Desafortunadamente, mientras que la justificación sugiere fuertemente que los autores del Estándar pretendían describir un estándar para la conformidad mínima en casos en los que un compilador de otra manera no tendría ninguna razón para creer que las cosas podrían alias , la rule no requiere que los compiladores reconozcan el aliasing en los casos en los que es obvio y los autores de gcc han decidido que preferirían generar el programa más pequeño que pueda mientras se ajusta al lenguaje mal escrito del Estándar, que generar código que es realmente útil, y en lugar de reconocer el aliasing en los casos en los que es obvio (sin dejar de ser capaz de asumir que las cosas que no parecen que se alias, no lo harán) memcpy, por lo que requiere un compilador para permitir la posibilidad de que los punteros de origen desconocido podrían alias casi cualquier cosa, lo que impide la optimización.

 0
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-13 21:04:05

Aparentemente el estándar permite que sizeof(char*) sea diferente de sizeof(int*), por lo que gcc se queja cuando intenta un cast directo. vacío * es un poco especial en que todo puede ser convertido de ida y vuelta hacia y desde vacío*. En la práctica, no conozco muchas arquitecturas / compiladores donde un puntero no es siempre el mismo para todos los tipos, pero gcc tiene razón al emitir una advertencia incluso si es molesto.

Creo que el camino seguro sería

int i, *p = &i;
char *q = (char*)&p[0];

O

char *q = (char*)(void*)p;

También puedes probar esto y vea lo que obtiene:

char *q = reinterpret_cast<char*>(p);
 -4
Author: Sebastien Mirolo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-16 08:25:20