La desreferenciación del puntero con juegos de palabras romperá las reglas estrictas de alias

10 minutos de lectura

avatar de usuario
enmarcador

Usé el siguiente 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

Googleando encontré dos respuestas ortogonales:

contra

  • Entonces, básicamente, si tiene un int* y un float*, no se les permite apuntar a la misma ubicación de memoria. Si su código no respeta esto, lo más probable es que el optimizador del compilador rompa su código.

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

[update] Sustituí el ejemplo del juguete con la función real.

  • Su función devuelve un doble, pero convierte su retorno en un int. ¿Por qué no lanzar al doble?

    -Adam Shiemke

    14 de julio de 2010 a las 13:09

  • Mi lectura de los enlaces proporcionados: el enlace bytes.com parece ser mayormente incorrecto (en realidad, las cosas han cambiado desde el lanzamiento de GCC 4.x), mientras que el enlace SO parece estar bien. Ver C99, “6.5 Expresiones”, cláusula 7.

    – Dummy00001

    14 de julio de 2010 a las 14:25

  • Estoy un poco confundido por el mensaje de error porque pensé que las reglas de alias estaban excluidas char tipos (es decir, un char puntero siempre se le permite alias de otros punteros a menos que sea restricted.) Tal vez tengas que hacerlo unsigned char para que se aplique..? Me interesaría ver la respuesta correcta.

    – R.. GitHub DEJA DE AYUDAR A ICE

    14 de julio de 2010 a las 14:53

  • @RA char * puede alias cualquier cosa, pero no al revés. Está lanzando y desreferenciando char a short,int,float y double en el código anterior.

    – 5sonidos

    14 de julio de 2010 a las 16:50

avatar de usuario
Lasse Reinhold

El problema ocurre porque accede a una matriz de caracteres 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 diferente tipo. Esta suposición se denomina alias estricto y permite que el compilador realice algunas optimizaciones:

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

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

Lo más probable es que el ciclo esté optimizado y termines con solo:

return *(double*)data;

Que deja tus datos[] no inicializado En este caso particular, el compilador podría ver que sus punteros se superponen, pero si lo hubiera declarado char* datapodría haber dado errores.

Pero, la regla de alias estricto dice que char* y void* pueden apuntar a cualquier tipo. Entonces puedes reescribirlo en:

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

Es muy importante comprender o corregir las advertencias de alias estrictas. 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.

avatar de usuario
Martín B.

Parece que realmente quieres usar fread:

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

Dicho esto, si desea seguir la ruta de leer caracteres, luego reinterpretarlos 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. Supongo que quería 4 bytes; al menos no conozco ningún sistema donde un int sea de 3 bytes.

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

Editar: si desea leer entradas de varias longitudes de un archivo, de forma portátil, intente algo como esto:

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

(Nota: en un programa real, también querrá probar el valor de retorno de fgetc() contra EOF).

Esto lee 4 bytes sin firmar del archivo en formato little-endian, independientemente de cuál es la endianidad del sistema. Debería funcionar en casi cualquier sistema donde un sin firmar tenga al menos 4 bytes.

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

  • +1. Para enfatizar nuevamente: una unión es una forma oficial de mantener el código en estricto cumplimiento con los alias. Esto no es específico de gcc, es solo que el optimizador de gcc está más roto al respecto. Las advertencias no deben ignorarse: deshabilite explícitamente la optimización de aliasing -fstrict o corrija el código.

    – Dummy00001

    14 de julio de 2010 a las 14:17

  • @Framester: Depende de lo que quieras portar. La mayoría de los sistemas de escritorio y parientes significan lo mismo por un 32-bit intpero algunos son big-endian y algunos son small-endian, es decir, el orden de los bytes en el int puede variar.

    –David Thornley

    14 de julio de 2010 a las 16:56

  • @David: solo para elegir una liendre: el término habitual es “little-endian”.

    – Martín B.

    15 de julio de 2010 a las 8:12

  • @Dummy00001 “una unión es una forma oficial de mantener el código en estricto cumplimiento con los alias.” ¿Según la OMS?

    – chico curioso

    3 oct 2011 a las 18:42

  • @kestasx ver §6.2.6.1 ¶7: los bytes… que no corresponden a ese miembro pero sí a otros miembros toman valores no especificados, lo que implica que los bytes se pueden reinterpretar leyendo a través de un miembro diferente. Además, esto fue objeto de una corrección en ISO C99 TC3 (DR283)

    – ninjalj

    13/04/2015 a las 18:52

Usar una unión es no lo correcto que hacer 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).

  • de un miembro no escrito del sindicato no está definidoEn este caso sencillo: union U { int i; short s; } u; u.s=1; return u.i;, sí. En general, depende.

    – chico curioso

    3 oct 2011 a las 20:03

  • En C la unión es un comportamiento bien definido; en C++ es un comportamiento indefinido.

    –MM

    24 de diciembre de 2014 a las 9:46

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

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

return *(short*)data;

Con este:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;

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

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

No hagas nada de eso. El problema de alias sobre el que advierte gcc es inocente en comparación con lo que está haciendo.

  • Hola, sustituí el ejemplo del juguete con la función real. Y el int con 3 bytes fue solo un error tipográfico de mi parte.

    – Framester

    14 de julio de 2010 a las 16:40

avatar de usuario
Super gato

Los autores del Estándar C querían permitir que los escritores de compiladores generaran código eficiente en circunstancias en las que sería teóricamente posible pero poco probable que se pudiera acceder a su valor de una variable global usando un puntero aparentemente no relacionado. La idea no era prohibir el juego de palabras mediante la conversión y la desreferenciación de un puntero en una sola expresión, sino más bien decir eso 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 ser un alias global aparentemente no relacionado, sin requerir que los tipos coincidan perfectamente. Desafortunadamente, si bien la justificación sugiere fuertemente que los autores del Estándar intentaron describir un estándar para el cumplimiento mínimo en los casos en que un compilador de lo contrario no tendría ninguna razón para creer que las cosas podrían aliasla regla no exige que los compiladores reconozcan los alias en los casos en que sea evidente y los autores de gcc han decidido que prefieren generar el programa más pequeño posible mientras se ajusta al lenguaje mal escrito del Estándar, que generar código que es realmente útil, y en lugar de reconocer el alias en los casos en que es obvio (mientras que aún siendo capaz de asumir que las cosas que no parecen ser alias, no lo serán) preferirían requerir que los programadores usen memcpylo que requiere un compilador para permitir la posibilidad de que los punteros de origen desconocido puedan crear alias de casi cualquier cosa, lo que impide la optimización.

  • Hola, sustituí el ejemplo del juguete con la función real. Y el int con 3 bytes fue solo un error tipográfico de mi parte.

    – Framester

    14 de julio de 2010 a las 16:40

avatar de usuario
Sebastián Mirolo

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

Creo que la forma segura sería

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

o

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

También puedes probar esto y ver lo que obtienes:

char *q = reinterpret_cast<char*>(p);

  • reinterpret_cast es C++. esto es c

    – tomate

    16 de agosto de 2010 a las 8:29

  • el estándar permite que sizeof(char*) sea diferente de sizeof(int*)“o podrían tener el mismo tamaño pero una representación diferente, pero de todos modos esto no tiene nada que ver con el problema aquí. Esta pregunta es sobre juegos de palabras, no representación de punteros”.char *q = (char*)&p[0]” el problema no es cómo hacer que dos punteros de diferentes tipos apunten a la misma dirección. Esta pregunta se trata de juegos de palabras, no de conversión de punteros.

    – chico curioso

    03/10/2011 a las 20:00


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad