Violando el estricto aliasing en C, incluso sin ningún casting?


¿Cómo pueden *i y u.i imprimir diferentes números en este código, a pesar de que i se define como int *i = &u.i;? Solo puedo asumir que estoy activando UB aquí, pero no puedo ver cómo exactamente.

(ideone demo se replica si selecciono 'C' como idioma. Pero como @2501 señaló, no si 'C99 estricto' es el lenguaje. Pero de nuevo, tengo el problema con gcc-5.3.0 -std=c99!)

// gcc       -fstrict-aliasing -std=c99   -O2
union
{   
    int i;
    short s;
} u;

int     * i = &u.i;
short   * s = &u.s;

int main()
{   
    *i  = 2;
    *s  = 100;

    printf(" *i = %d\n",  *i); // prints 2
    printf("u.i = %d\n", u.i); // prints 100

    return 0;
}

(gcc 5.3.0, con -fstrict-aliasing -std=c99 -O2, también con -std=c11)

Mi teoría es que 100 es el "correcto" respuesta, porque el escribir al miembro del sindicato a través del short-lvalue *s se define como tal (para esta plataforma/endianidad/lo que sea). Pero creo que el optimizador no se da cuenta de que la escritura a *s puede alias u.i, y por lo tanto piensa que *i=2; es la única línea que puede afectar *i. ¿Es una teoría razonable?

Si *s puede alias u.i, y u.i puede alias *i, entonces seguramente el compilador debe pensar que *s puede alias *i? No debería ser aliasing 'transitivo'?

Finalmente, siempre tuve esta suposición de que los problemas de alias estrictos fueron causados por un mal casting. Pero no hay casting en esto!

(Mi experiencia es C++, espero estar haciendo una pregunta razonable sobre C aquí. Mi (limitado) entendimiento es que, en C99, es aceptable escribir a través de un miembro del sindicato y luego leer a través de otro miembro de un tipo diferente.)

Author: Aaron McDaid, 2016-09-29

7 answers

El deterioro es emitido por la opción de optimización -fstrict-aliasing. Su comportamiento y posibles trampas se describen en documentación del CCG :

Presta especial atención a código como este:

      union a_union {
        int i;
        double d;
      };

      int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

La práctica de leer de un miembro diferente del sindicato más recientemente escrito a (llamado "tipo-juego de palabras") es común. Incluso con -fstrict-aliasing, se permite el juego de tipo, siempre que se acceda a la memoria a través del tipo de unión. Por lo tanto, el código anterior funciona como previsto. Ver Estructuras uniones enumeraciones e implementación de campos de bits. Sin embargo, este código podría no:

      int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

Tenga en cuenta que la implementación conforme está perfectamente permitida para aprovechar esta optimización, ya que el segundo ejemplo de código muestra un comportamiento indefinido. Véase las respuestas de Olaf y otras para referencia.

 56
Author: Grzegorz Szpetkowski,
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
2017-09-14 04:11:35

C estándar (es decir, C11, n1570), 6.5p7 :

Un objeto tendrá acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos:

  • ...
  • un tipo agregado o de unión que incluya entre sus miembros uno de los tipos antes mencionados (incluido, recursivamente, un miembro de un subagregado o sindicato contenido), o un tipo de carácter.

Las expresiones lvalue de sus punteros son no union tipos, por lo que esta excepción no se aplica. El compilador es correcto explotando este comportamiento indefinido.

Haga que los tipos de los punteros apunten al tipo union y desreferencie con el miembro respectivo. Eso debería funcionar:

union {
    ...
} u, *i, *p;
 18
Author: too honest for this site,
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-09-30 16:39:08

El aliasing estricto no está especificado en el estándar C, pero la interpretación habitual es que el aliasing de unión (que reemplaza al aliasing estricto) solo se permite cuando se accede directamente a los miembros de la unión por nombre.

Para fundamentar esto considere:

void f(int *a, short *b) { 

La intención de la regla es que el compilador pueda asumir que a y b no se alias, y generar código eficiente en f. Pero si el compilador tuviera que tener en cuenta el hecho de que a y b podrían superponerse miembros del sindicato, en realidad no podía hacer esas suposiciones.

Si los dos punteros son o no parámetros de función es irrelevante, la estricta regla de aliasing no diferencia en función de eso.

 12
Author: M.M,
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-09-29 06:49:54

Este código de hecho invoca UB, porque no respeta la estricta regla de alias. n1256 draft of C99 states in 6.5 Expressions §7:

Un objeto tendrá acceso a su valor almacenado solo por una expresión lvalue que tenga uno de los siguientes tipos:
- un tipo compatible con el tipo efectivo del objeto,
- una versión cualificada de un tipo compatible con el tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente al tipo efectivo de la objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada de la tipo efectivo del objeto,
- un tipo agregado o de unión que incluya uno de los tipos antes mencionados entre sus miembros (incluyendo, recursivamente, un miembro de un subagregado o sindicato contenido), o
- un tipo de carácter.

Entre el *i = 2; y el printf(" *i = %d\n", *i); solo se modifica un objeto corto. Con la ayuda de la estricta regla de aliasing, el compilador es libre de asumir que el objeto int apuntado por i no ha sido cambiado, y puede usar directamente un valor en caché sin recargarlo de la memoria principal.

Evidentemente no es lo que un ser humano normal esperaría, pero la estricta regla de aliasing fue escrita precisamente para permitir que los compiladores de optimización usen valores en caché.

Para la segunda impresión, las uniones son referenciadas en el mismo estándar en 6.2.6.1 Representaciones de tipos / General §7:

Cuando un valor se almacena en un miembro de un objeto de tipo union, los bytes del objeto representación que no corresponde a ese miembro pero sí a otros miembros tome valores no especificados.

Así como u.s se ha almacenado, {[4] } han tomado un valor no especificado por estándar

Pero podemos leer más adelante en 6.5.2.3 Estructura y miembros del sindicato §3 nota 82:

Si el miembro utilizado para acceder a los contenidos de una union object no es el mismo que el miembro utilizado por última vez para almacenar un valor en el objeto, la parte apropiada de la representación del objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado " tipo punning"). Esto podría ser una representación trampa.

Aunque las notas no son normativas, permiten una mejor comprensión del estándar. Cuando u.s se han almacenado a través del puntero *s, los bytes correspondiente a un corto se han cambiado al valor 2. Suponiendo un sistema endian poco, como 100 es más pequeño que el valor de un corto, la representación como un int ahora debe ser 2 como bytes de orden alto eran 0.

TL/DR: incluso si no es normativo, la nota 82 debería requerir que en un sistema endiano pequeño de las familias x86 o x64, printf("u.i = %d\n", u.i); imprime 2. Pero según la estricta regla de aliasing, el compilador todavía puede asumir que el valor apuntado por i no ha cambiado y puede imprimir 100

 7
Author: Serge Ballesta,
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-10-04 19:08:58

Estás sondeando un área algo controvertida del estándar C.

Esta es la estricta regla de aliasing:

Un objeto tendrá acceso a su valor almacenado solo por un lvalue expresión que tiene uno de los siguientes tipos:

  • un tipo compatible con el tipo efectivo del objeto,
  • una versión calificada de un tipo compatible con el tipo efectivo del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a el tipo efectivo del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una subagregada o unión contenida),
  • un tipo de carácter.

(C2011, 6.5/7)

La expresión lvalue *i tiene tipo int. La expresión lvalue *s tiene el tipo short. Estos tipos no son compatibles entre sí, ni ambos compatibles con ningún otro tipo en particular, ni la regla de alias estricta ofrece ninguna otra alternativa que permita que ambos accesos se ajusten si los punteros están aliadizados.

Si al menos uno de los accesos no es conforme, entonces el comportamiento es indefinido, por lo que el resultado que informe is o cualquier otro resultado en absoluto is es totalmente aceptable. En la práctica, el compilador debe producir código que reordene las asignaciones con las llamadas printf(), o que use un valor previamente cargado de *i desde un registro en lugar de volver a leerlo desde la memoria, o algo similar.

La controversia antes mencionada surge porque la gente a veces señalará nota al pie 95:

Si el miembro utilizado para leer el contenido de un objeto union no es el mismo que el miembro utilizado por última vez para almacenar un valor en el objeto, la parte apropiada del objeto la representación del valor se reinterpreta como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado "tipo punning"). Esto podría ser una representación trampa.

Las notas al pie son informativas, sin embargo, no normativas, por lo que realmente no hay duda de qué texto gana si entran en conflicto. Personalmente, tomo la nota simplemente como una guía de implementación, aclarando el significado del hecho de que el almacenamiento para los miembros del sindicato superponer.

 6
Author: John Bollinger,
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-09-28 21:45:12

Parece que esto es el resultado de que el optimizador hace su magia.

Con -O0, ambas líneas imprimen 100 como se esperaba (asumiendo little-endian). Con -O2, hay algunos reordenamientos en curso.

Gdb da la siguiente salida:

(gdb) start
Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14.
Starting program: /tmp/x1
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000

Temporary breakpoint 1, main () at /tmp/x1.c:14
14      {
(gdb) step
15          *i  = 2;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
15          *i  = 2;
(gdb)
16          *s  = 100;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
 *i = 2
19          printf("u.i = %d\n", u.i); // prints 100
(gdb)
u.i = 100
22      }
(gdb)
0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6
(gdb)

La razón por la que esto sucede, como otros han dicho, es porque es un comportamiento indefinido acceder a una variable de un tipo a través de un puntero a otro tipo, incluso si la variable en cuestión es parte de una unión. Así que el optimizador es libre de hacer como desea en este caso.

La variable del otro tipo solo se puede leer directamente a través de una unión que garantiza un comportamiento bien definido.

Lo curioso es que incluso con -Wstrict-aliasing=2, gcc (a partir de 4.8.4) no se queja de este código.

 5
Author: dbush,
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-10-04 19:11:17

Ya sea por accidente o por diseño, C89 incluye lenguaje que ha sido interpretado de dos maneras diferentes (junto con varias interpretaciones intermedias). Se trata de la cuestión de cuándo se debe exigir a un compilador que reconozca que el almacenamiento utilizado para un tipo puede accederse a través de punteros de otro. En el ejemplo dado en la justificación C89, el aliasing se considera entre una variable global que claramente no forma parte de ninguna unión y un puntero a un tipo diferente, y nada en el código sugiere que pueda ocurrir el aliasing.

Una interpretación paraliza horriblemente el lenguaje, mientras que la otra restringiría el uso de ciertas optimizaciones a modos "no conformes". Si aquellos que no tenían sus optimizaciones preferidas otorgadas a un estatus de segunda clase hubieran escrito C89 para que coincidiera inequívocamente con su interpretación, esas partes del Estándar habrían sido ampliamente denunciadas y habría habido algún tipo de reconocimiento claro de una dialecto de C que honraría la interpretación no paralizante de las reglas dadas.

Desafortunadamente, lo que ha sucedido en su lugar es que dado que las reglas claramente no requieren que los escritores de compiladores apliquen una interpretación paralizante, la mayoría de los escritores de compiladores durante años simplemente interpretaron las reglas de una manera que conserva la semántica que hizo que C fuera útil para la programación de sistemas; los programadores no tenían ninguna razón para quejarse de que desde su perspectiva, parecía obvio para todos que debían hacerlo a pesar de la negligencia del Estándar. Mientras tanto, sin embargo, algunas personas insisten en que dado que el Estándar siempre ha permitido a los compiladores procesar un subconjunto semánticamente debilitado del lenguaje de programación de sistemas de Ritchie, no hay ninguna razón por la que se deba esperar que un compilador conforme al estándar procese cualquier otra cosa.

La resolución sensata para este asunto sería reconocer que C se usa para varios propósitos que deberían haber múltiples modos de compilación one un modo requerido trataría a todos los accesos de todo cuya dirección fue tomada como si leyeran y escribieran el almacenamiento subyacente directamente, y sería compatible con el código que espera cualquier nivel de soporte de punning tipo basado en puntero. Otro modo podría ser más restrictivo que C11, excepto cuando el código usa explícitamente directivas para indicar cuándo y dónde debe almacenarse un tipo que se ha utilizado reinterpretado o reciclado para su uso como otro. Otros modos permitirían algunas optimizaciones pero soportarían algún código que se rompería bajo dialectos más estrictos; los compiladores sin soporte específico para un dialecto particular podrían sustituir uno con comportamientos de aliasing más definidos.

 1
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-10-03 17:06:09