gcc, aliasing estricto y fundición a través de un sindicato

¿Tienes alguna historia de terror que contar? El Manual de GCC recientemente agregó una advertencia con respecto a -flexible-aliasing y lanzar un puntero a través de una unión:

[…] Tomar la dirección, arrojar el puntero resultante y desreferenciar el resultado tiene un comportamiento indefinido [énfasis agregado], incluso si el elenco usa un tipo de unión, por ejemplo:

union a_union { int i; double d; }; int f() { double d = 3.0; return ((union a_union *)&d)->i; } 

¿Alguien tiene un ejemplo para ilustrar este comportamiento indefinido?

Tenga en cuenta que esta pregunta no se trata de lo que dice el estándar C99, o no dice. Se trata del funcionamiento real de gcc y otros comstackdores existentes hoy en día.

Solo estoy adivinando, pero un problema potencial puede estar en el ajuste de d a 3.0. Como d es una variable temporal que nunca se lee directamente, y que nunca se lee mediante un puntero “algo compatible”, el comstackdor puede no molestarse en configurarlo. Y luego f () devolverá algo de basura de la stack.

Mi bash simple e ingenuo fracasa. Por ejemplo:

 #include  union a_union { int i; double d; }; int f1(void) { union a_union t; td = 3333333.0; return ti; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3) } int f2(void) { double d = 3333333.0; return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior' } int main(void) { printf("%d\n", f1()); printf("%d\n", f2()); return 0; } 

funciona bien, dando en CYGWIN:

 -2147483648 -2147483648 

Al observar al ensamblador, vemos que gcc optimiza completamente t : f1() simplemente almacena la respuesta precalculada:

 movl $-2147483648, %eax 

mientras que f2() empuja 3333333.0 en la stack de coma flotante , y luego extrae el valor de retorno:

 flds LC0 # LC0: 1246458708 (= 3333333.0) (--> 80 bits) fstpl -8(%ebp) # save in d (64 bits) movl -8(%ebp), %eax # return value (32 bits) 

Y las funciones también están en línea (lo que parece ser la causa de algunos errores sutiles de alias estrictos) pero eso no es relevante aquí. (Y este ensamblador no es tan relevante, pero agrega detalles corroborativos).

También tenga en cuenta que tomar direcciones es obviamente incorrecto (o correcto , si está tratando de ilustrar el comportamiento indefinido). Por ejemplo, así como sabemos que esto está mal:

 extern void foo(int *, double *); union a_union t; td = 3.0; foo(&t.i, &t.d); // undefined behavior 

también sabemos que esto está mal:

 extern void foo(int *, double *); double d = 3.0; foo(&((union a_union *)&d)->i, &d); // undefined behavior 

Para una discusión de antecedentes sobre esto, vea por ejemplo:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http://davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
(= página de búsqueda en Google luego ver página en caché)

¿Cuál es la regla de aliasing estricta?
C99 reglas estrictas de aliasing en C ++ (GCC)

En el primer enlace, borradores de actas de una reunión de ISO hace siete meses, un participante señala en la sección 4.16:

¿Hay alguien que piense que las reglas son lo suficientemente claras? Nadie es realmente capaz de interpretarlos.

Otras notas: Mi prueba fue con gcc 4.3.4, con -O2; opciones -O2 y -O3 implican -fstrict-aliasing. El ejemplo del Manual de GCC asume sizeof (double) > = sizeof (int); no importa si son desiguales.

Además, como lo señala Mike Acton en el enlace cellperformace, -Wstrict-aliasing=2 , pero no =3 , produce una warning: dereferencing type-punned pointer might break strict-aliasing rules para el ejemplo aquí.

    El hecho de que GCC advierta sobre los sindicatos no significa necesariamente que los sindicatos actualmente no funcionen. Pero aquí hay un ejemplo un poco menos simple que el tuyo:

     #include  struct B { int i1; int i2; }; union A { struct B b; double d; }; int main() { double d = 3.0; #ifdef USE_UNION ((union A*)&d)->b.i2 += 0x80000000; #else ((int*)&d)[1] += 0x80000000; #endif printf("%g\n", d); } 

    Salida:

     $ gcc --version gcc (GCC) 4.3.4 20090804 (release) 1 Copyright (C) 2008 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gcc -oalias alias.c -O1 -std=c99 && ./alias -3 $ gcc -oalias alias.c -O3 -std=c99 && ./alias 3 $ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias -3 $ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias -3 

    Entonces, en GCC 4.3.4, la unión “guarda el día” (asumiendo que quiero la salida “-3”). Deshabilita la optimización que se basa en un alias estricto y que da como resultado la salida “3” en el segundo caso (solamente). Con -Wall, USE_UNION también desactiva la advertencia de tipo de punteo.

    No tengo gcc 4.4 para probar, pero por favor, dale un vistazo a este código. Su código en efecto comprueba si la memoria para d se inicializa antes de volver a leerse a través de una unión: la mina prueba si se modificó.

    Por cierto, la manera segura de leer la mitad de un doble como int es:

     double d = 3; int i; memcpy(&i, &d, sizeof i); return i; 

    Con la optimización en GCC, esto da como resultado:

      int thing() { 401130: 55 push %ebp 401131: 89 e5 mov %esp,%ebp 401133: 83 ec 10 sub $0x10,%esp double d = 3; 401136: d9 05 a8 20 40 00 flds 0x4020a8 40113c: dd 5d f0 fstpl -0x10(%ebp) int i; memcpy(&i, &d, sizeof i); 40113f: 8b 45 f0 mov -0x10(%ebp),%eax return i; } 401142: c9 leave 401143: c3 ret 

    Entonces no hay una llamada real a memcpy. Si no estás haciendo esto, te mereces lo que obtienes si los sindicatos dejan de trabajar en GCC 😉

    Su afirmación de que el siguiente código es “incorrecto”:

     extern void foo(int *, double *); union a_union t; td = 3.0; foo(&t.i, &t.d); // undefined behavior 

    … Está Mal. Simplemente tomar la dirección de los dos miembros del sindicato y pasarlos a una función externa no da como resultado un comportamiento indefinido; solo obtienes desreferenciar uno de esos punteros de forma inválida. Por ejemplo, si la función foo regresa inmediatamente sin desreferenciar los punteros que le pasó, entonces el comportamiento no está indefinido. Con una lectura estricta del estándar C99, incluso hay algunos casos en que los punteros se pueden desreferenciar sin invocar un comportamiento indefinido; por ejemplo, podría leer el valor al que hace referencia el segundo puntero, y luego almacenar un valor a través del primer puntero, siempre y cuando ambos apunten a un objeto dinámicamente asignado (es decir, uno sin un “tipo declarado”).

    El aliasing ocurre cuando el comstackdor tiene dos punteros diferentes para la misma pieza de memoria. Al encasillar un puntero, está generando un nuevo puntero temporal. Si el optimizador reordena las instrucciones de ensamblaje, por ejemplo, acceder a los dos punteros podría dar dos resultados totalmente diferentes: podría reordenar una lectura antes de escribir en la misma dirección. Es por eso que es un comportamiento indefinido.

    Es poco probable que veas el problema en un código de prueba muy simple, pero aparecerá cuando haya mucho en juego.

    Creo que la advertencia es dejar en claro que los sindicatos no son un caso especial, a pesar de que es de esperar que lo sean.

    Consulte este artículo de Wikipedia para obtener más información sobre aliasing: http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization