Pregunta ¿Cuál es la regla de aliasing estricta?


Al preguntar acerca de Comportamiento indefinido común en C, almas más iluminadas de lo que me refería a la estricta regla de aliasing.
De qué están hablando?


668
2017-09-19 01:30


origen


Respuestas:


Una situación típica con la que se encuentran problemas estrictos de alias es cuando se superpone una estructura (como un mensaje de red / dispositivo / red) en un búfer del tamaño de palabra de su sistema (como un puntero a uint32_ts o uint16_ts). Cuando superpone una estructura en dicho búfer, o un búfer en dicha estructura a través de la fundición de puntero, puede violar fácilmente las reglas de alias estrictas.

Entonces, en este tipo de configuración, si quiero enviar un mensaje a algo, tendría que tener dos punteros incompatibles apuntando al mismo trozo de memoria. Entonces podría ingenuamente codificar algo como esto:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

La regla de aliasing estricta hace que esta configuración sea ilegal: desreferenciar un puntero que alias un objeto que no es de tipo compatible o uno de los otros tipos permitidos por C 2011 6.5 párrafo 71 es un comportamiento indefinido Desafortunadamente, todavía puedes codificar de esta manera, tal vez recibe algunas advertencias, compila bien, solo para tener un extraño comportamiento inesperado cuando ejecutas el código.

(GCC parece algo inconsistente en su capacidad de dar advertencias de aliasing, a veces nos da una advertencia amistosa y a veces no).

Para ver por qué este comportamiento no está definido, tenemos que pensar en lo que la regla de alias estricto compra al compilador. Básicamente, con esta regla, no tiene que pensar en insertar instrucciones para actualizar el contenido de buff cada carrera del bucle. En cambio, al optimizar, con algunas suposiciones desalentadoramente no aplicadas sobre el aliasing, puede omitir esas instrucciones, cargar buff[0] y buff[1] en registros de la CPU una vez antes de ejecutar el bucle, y acelerar el cuerpo del bucle. Antes de que se introdujera el aliasing estricto, el compilador tenía que vivir en un estado de paranoia que el contenido de buff podría cambiar en cualquier momento desde cualquier lugar por cualquiera. Por lo tanto, para obtener una ventaja de rendimiento adicional, y suponiendo que la mayoría de la gente no utiliza punteros de tipo de tipo de letra, se introdujo la regla de aliasing estricta.

Tenga en cuenta que, si cree que el ejemplo es artificial, esto podría suceder si pasa un búfer a otra función que hace el envío por usted, si es que lo tiene.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Y reescribimos nuestro ciclo anterior para aprovechar esta conveniente función

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

El compilador puede o no ser lo suficientemente inteligente como para tratar de enviar SendMessage en línea y puede o no decidir cargar o no cargar de nuevo. Si SendMessage es parte de otra API que se compila por separado, probablemente tiene instrucciones para cargar los contenidos de buff. Por otra parte, tal vez estés en C ++ y esta es solo una implementación de encabezado con plantillas que el compilador cree que puede alinearse. O tal vez es solo algo que escribió en su archivo .c para su propia conveniencia. De todos modos, el comportamiento indefinido podría seguir. Incluso cuando sabemos algo de lo que sucede debajo del capó, sigue siendo una violación de la regla, por lo que no se garantiza un comportamiento bien definido. Entonces, simplemente envolviendo una función que toma nuestro buffer delimitado por palabras no necesariamente ayuda.

Entonces, ¿cómo puedo solucionar esto?

  • Usa una unión. La mayoría de los compiladores lo admiten sin quejarse sobre el aliasing estricto. Esto está permitido en C99 y explícitamente permitido en C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Puede deshabilitar el alias estricto en su compilador (f [no-] strict-aliasing en gcc))

  • Puedes usar char* para aliasing en lugar de la palabra de su sistema. Las reglas permiten una excepción para char* (incluso signed char y unsigned char) Siempre se supone que char* aliases otros tipos. Sin embargo, esto no funcionará de la otra manera: no se supone que su estructura alía un buffer de caracteres.

Principiante ten cuidado

Este es solo un campo de minas potencial al superponer dos tipos entre sí. También deberías aprender sobre endianness, alineación de palabrasy cómo lidiar con problemas de alineación a través de estructuras de embalaje correctamente.

Nota

1 Los tipos que C 2011 6.5 7 permite que un lvalue acceda son:

  • 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 no firmado correspondiente al 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 agregado o tipo de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subaggregado o sindicato contenido), o
  • un tipo de personaje

479
2017-09-19 01:38



La mejor explicación que he encontrado es por Mike Acton, Entender el Alias ​​estricto. Se enfoca un poco en el desarrollo de PS3, pero eso es básicamente solo GCC.

Del artículo:

"Alias ​​estricto es una suposición, hecha por el compilador C (o C ++), que desmarcando punteros a objetos de diferentes tipos nunca se referirá a la misma ubicación de memoria (es decir, alias entre sí)".

Entonces, básicamente, si tienes un int* apuntando a un poco de memoria que contiene un int y luego señalas un float* a esa memoria y usarlo como un float rompes la regla Si su código no respeta esto, entonces el optimizador del compilador muy probablemente romperá su código.

La excepción a la regla es una char*, que está permitido apuntar a cualquier tipo.


210
2017-08-10 04:43



Esta es la regla de alias estricto, que se encuentra en la sección 3.10 de la C ++ 03 estándar (otras respuestas proporcionan una buena explicación, pero ninguna proporciona la regla en sí):

Si un programa intenta acceder al valor almacenado de un objeto a través de un lvalor distinto de uno de los siguientes tipos, el comportamiento no está definido:

  • el tipo dinámico del objeto,
  • una versión cv-calificada del tipo dinámico del objeto,
  • un tipo que es el tipo firmado o no firmado correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,
  • un agregado o tipo de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subaggregado o sindicato contenido),
  • un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
  • un char o unsigned char tipo.

C ++ 11 y C ++ 14 redacción (cambios enfatizados):

Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue de otro que no sea uno de los siguientes tipos, el comportamiento no está definido:

  • el tipo dinámico del objeto,
  • una versión cv-calificada del tipo dinámico del objeto,
  • un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o no firmado correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,
  • un agregado o tipo de unión que incluye uno de los tipos mencionados entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o miembro de datos no estáticos de un subaggregado o unión contenida),
  • un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
  • un char o unsigned char tipo.

Dos cambios fueron pequeños: glvalue en lugar de lvaluey aclaración del caso agregado / sindicato.

El tercer cambio hace una garantía más fuerte (relaja la regla de aliasing fuerte): el nuevo concepto de tipos similares que ahora son seguros para alias.


También el do redacción (C99; ISO / IEC 9899: 1999 6.5 / 7; la misma redacción se utiliza en ISO / CEI 9899: 2011 §6.5 ¶7):

Un objeto debe tener su valor almacenado al que se accede solo por un valor l   expresión que tiene uno de los siguientes tipos  73) u 88):

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

 73) u 88) La intención de esta lista es especificar las circunstancias en las que un objeto puede o no tener alias.


124
2018-06-19 23:46



El alias estricto no se refiere solo a punteros, también afecta a las referencias, escribí un artículo sobre el wiki del desarrollador de impulso y fue tan bien recibido que lo convertí en una página en mi sitio web de consultoría. Explica completamente qué es, por qué confunde tanto a la gente y qué hacer al respecto. Estricto Aliasing White Paper. En particular, explica por qué los sindicatos son un comportamiento arriesgado para C ++, y por qué el uso de memcpy es el único arreglo portátil tanto en C como en C ++. Espero que esto sea útil.


39
2018-05-14 02:37



Como una adición a lo que Doug T. ya escribió, aquí es un caso de prueba simple que probablemente lo desencadena con gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Compilar con gcc -O2 -o check check.c . Usualmente (con la mayoría de las versiones de gcc que probé) se genera un "problema de alias estricto", porque el compilador asume que "h" no puede ser la misma dirección que "k" en la función de "verificación". Por eso, el compilador optimiza if (*h == 5) de distancia y siempre llama al printf.

Para aquellos que están interesados ​​aquí está el código ensamblador x64, producido por gcc 4.6.3, ejecutándose en ubuntu 12.04.2 para x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Entonces, la condición if se ha ido completamente del código del ensamblador.


30
2017-09-19 01:38



Tipo de juego de palabras a través de modelos de puntero (en lugar de usar una unión) es un ejemplo importante de romper el alias estricto.


15
2018-04-26 22:42



De acuerdo con el razonamiento C89, los autores del Estándar no querían requerir que los compiladores recibieran un código como:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

se debe exigir que vuelva a cargar el valor de x entre la declaración de asignación y devolución para permitir la posibilidad de que p podría apuntar a xy la asignación a *p podría en consecuencia alterar el valor de x. La noción de que un compilador debería tener el derecho de suponer que no habrá aliasing en situaciones como la anterior no fue controvertido

Desafortunadamente, los autores del C89 escribieron su regla de una manera que, si se leyera literalmente, haría incluso que la siguiente función invoque el Comportamiento no definido:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

porque usa un lvalue de tipo int para acceder a un objeto de tipo struct Sy int no se encuentra entre los tipos que se pueden usar accediendo a un struct S. Como sería absurdo tratar todo el uso de miembros de estructuras y sindicatos que no son de carácter como Comportamiento Indefinido, casi todos reconocen que hay al menos algunas circunstancias en las que se puede usar un valor l de un tipo para acceder a un objeto de otro tipo. . Desafortunadamente, el Comité de estándares de C no ha podido definir cuáles son esas circunstancias.

Gran parte del problema es resultado del Informe de defectos # 028, que preguntó sobre el comportamiento de un programa como:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

El Informe de defectos n. ° 28 establece que el programa invoca Comportamiento no definido porque la acción de escribir un miembro de unión de tipo "doble" y leer uno de tipo "int" invoca un comportamiento definido por la implementación. Tal razonamiento no tiene sentido, pero forma la base de las reglas de Tipo Eficaz que complican innecesariamente el lenguaje sin hacer nada para resolver el problema original.

La mejor manera de resolver el problema original sería tratar el nota al pie sobre el propósito de la regla como si fuera normativa, y hecha la regla no se puede hacer cumplir, excepto en casos que en realidad involucran accesos conflictivos usando alias. Dado algo como:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

No hay conflicto dentro inc_int porque todos los accesos al almacenamiento se acceden a través de *p se hacen con un lvalue de tipo inty no hay conflicto en test porque p se deriva visiblemente de una struct Sy para la próxima vez s se usa, todos los accesos a ese almacenamiento que alguna vez se realizarán a través de p ya habrá sucedido.

Si el código fue cambiado levemente ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Aquí, hay un conflicto de aliasing entre p y el acceso a s.x en la línea marcada porque en ese punto en ejecución existe otra referencia que se usará para acceder al mismo almacenamiento.

Si el Informe de defectos 028 dijera que el ejemplo original invocaba UB debido a la superposición entre la creación y el uso de los dos punteros, eso habría dejado las cosas mucho más claras sin tener que agregar "Tipos efectivos" u otra complejidad similar.


10
2017-12-24 12:04



Después de leer muchas de las respuestas, siento la necesidad de agregar algo:

Estricto alias (que describiré en un momento) es importante porque:

  1. El acceso a la memoria puede ser costoso (en cuanto al rendimiento), por lo que los datos se manipulan en los registros de la CPU antes de ser escrito nuevamente en la memoria física.

  2. Si los datos en dos registros de CPU diferentes se escribirán en el mismo espacio de memoria, no podemos predecir qué datos "sobrevivirán" cuando codificamos en C.

    En ensamblaje, donde codificamos manualmente la carga y descarga de los registros de la CPU, sabremos qué datos permanecen intactos. Pero C (afortunadamente) abstrae este detalle.

Como dos punteros pueden apuntar a la misma ubicación en la memoria, esto podría resultar en código complejo que maneja posibles colisiones.

Este código extra es lento y perjudica el rendimiento ya que realiza operaciones adicionales de lectura / escritura de memoria que son más lentas y (posiblemente) innecesarias.

los La estricta regla de aliasing nos permite evitar el código de máquina redundante en los casos en que debiera ser Es seguro suponer que dos punteros no apuntan al mismo bloque de memoria (ver también restrict palabra clave).

El alias estricto establece que es seguro suponer que los punteros a diferentes tipos apuntan a diferentes ubicaciones en la memoria.

Si un compilador nota que dos punteros apuntan a diferentes tipos (por ejemplo, un int * y un float *), asumirá que la dirección de memoria es diferente y no lo hará proteger contra colisiones de direcciones de memoria, lo que resulta en un código de máquina más rápido.

Por ejemplo:

Supongamos la siguiente función:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Para manejar el caso en el que a == b (ambos punteros apuntan a la misma memoria), necesitamos ordenar y probar la forma en que cargamos los datos de la memoria a los registros de la CPU, para que el código pueda terminar así:

  1. carga a y b de memoria.

  2. añadir a a b.

  3. salvar  b y recargar  a.

    (guardar desde el registro de la CPU a la memoria y cargar desde la memoria al registro de la CPU).

  4. añadir b a a.

  5. salvar a (desde el registro de la CPU) a la memoria.

El paso 3 es muy lento porque necesita acceder a la memoria física. Sin embargo, se requiere proteger contra instancias donde a y b apunta a la misma dirección de memoria.

El alias estricto nos permitiría evitar esto al decirle al compilador que estas direcciones de memoria son claramente diferentes (lo que, en este caso, permitirá una optimización aún mayor que no se puede realizar si los punteros comparten una dirección de memoria).

  1. Esto se puede contar al compilador de dos maneras, usando diferentes tipos para señalar. es decir.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Utilizando el restrict palabra clave. es decir.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Ahora, al cumplir con la regla de Alias ​​estricto, se puede evitar el paso 3 y el código se ejecutará significativamente más rápido.

De hecho, al agregar restrict palabra clave, toda la función podría optimizarse para:

  1. carga a y b de memoria.

  2. añadir a a b.

  3. guardar resultado tanto para ay para b.

Esta optimización no podría haberse hecho antes, debido a la posible colisión (donde a y b se triplicaría en lugar de duplicarse).


8
2017-07-08 02:07



Nota

Esto es un extracto de mi "¿Cuál es la regla de alias estricto y por qué nos importa?" redacción.

¿Qué es el aliasing estricto?

En C y C ++, el aliasing tiene que ver con los tipos de expresión a los que podemos acceder a través de los valores almacenados. Tanto en C como en C ++, el estándar especifica qué tipos de expresión se permiten para alias qué tipos. El compilador y el optimizador pueden suponer que seguimos estrictamente las reglas de aliasing, de ahí el término regla de aliasing estricta. Si intentamos acceder a un valor utilizando un tipo no permitido, se clasificará como comportamiento indefinido(UB) Una vez que tenemos un comportamiento indefinido, todas las apuestas se cancelan, los resultados de nuestro programa ya no son confiables.

Lamentablemente, con estrictas violaciones de alias, a menudo obtendremos los resultados que esperamos, dejando la posibilidad de que una versión futura de un compilador con una nueva optimización rompa el código que pensamos que era válido. Esto no es deseable y es un objetivo que vale la pena entender las reglas de aliasing estrictas y cómo evitar violarlas.

Para comprender más acerca de por qué nos preocupamos, discutiremos los problemas que surgen cuando se violan las reglas estrictas de aliasing, escriba el juego de palabras ya que las técnicas comunes utilizadas en el tipo de juego a menudo violan las reglas de alias estrictas y cómo escribir el juego de palabras correctamente.

Ejemplos preliminares

Veamos algunos ejemplos, luego podemos hablar exactamente de lo que dicen los estándares, examinar algunos ejemplos adicionales y luego ver cómo evitar el alias estricto y las violaciones de capturas que hemos omitido. Aquí hay un ejemplo que no debería ser sorprendente (ejemplo en vivo)

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Tenemos una En t* apuntando a la memoria ocupada por un En t y este es un aliasing válido. El optimizador debe suponer que las asignaciones a través de ip podría actualizar el valor ocupado por X.

El siguiente ejemplo muestra aliasing que conduce a un comportamiento indefinido (ejemplo en vivo)

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

En la función foo tomamos un En t* y un flotador*, en este ejemplo, llamamos foo y establecer ambos parámetros para que apunten a la misma ubicación de memoria que en este ejemplo contiene un En t. Nota la reinterpretar_cast le está diciendo al compilador que trate la expresión como si tuviera el tipo especificado por su parámetro de plantilla. En este caso, le estamos diciendo que trate la expresión &X como si tuviera tipo flotador*. Podemos ingenuamente esperar el resultado de la segunda cout ser 0 pero con la optimización habilitada usando -O2 tanto gcc como clang producen el siguiente resultado:

0
1

Lo cual no se puede esperar pero es perfectamente válido ya que hemos invocado un comportamiento indefinido. UN flotador no puede alias válidamente una En t objeto. Por lo tanto, el optimizador puede asumir el constante 1 almacenado cuando desreferencia yo será el valor de retorno desde una tienda a través de F no podría afectar válidamente una En t objeto. Al enchufar el código en el compilador Explorer, se muestra que esto es exactamente lo que está sucediendo (ejemplo en vivo)

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

El optimizador usando Análisis de alias basado en el tipo (TBAA) asume 1 será devuelto y mueve directamente el valor constante en el registro eax que lleva el valor de retorno. TBAA usa las reglas de idiomas sobre los tipos de alias permitidos para optimizar las cargas y las tiendas. En este caso, TBAA sabe que un flotador no puede alias y En t y optimiza la carga de yo.

Ahora, al Rule-Book

¿Qué dice exactamente el estándar que estamos permitidos y que no podemos hacer? El lenguaje estándar no es sencillo, así que para cada artículo intentaré proporcionar ejemplos de código que demuestren el significado.

¿Qué dice el estándar C11?

los C11 el estándar dice lo siguiente en la sección 6.5 Expresiones, párrafo 7:

Un objeto debe tener acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos:88)   - un tipo compatible con el tipo efectivo del objeto,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- una versión calificada de un tipo compatible con el tipo de objeto efectivo,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- un tipo que es el tipo firmado o no firmado correspondiente al tipo efectivo del objeto,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang tiene una extensión y además eso permite asignar unsigned int * a En t* a pesar de que no son tipos compatibles.

- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subaggregado o sindicato contenido), o

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- un tipo de personaje.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Qué dicen los C ++ 17 Draft Standard

El proyecto de norma C ++ 17 en la sección [basic.lval] párrafo 11 dice:

Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los siguientes tipos, el comportamiento no está definido:63   (11.1) - el tipo dinámico del objeto,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - una versión cv-calificada del tipo dinámico del objeto,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - un tipo similar (como se define en 7.5) al tipo dinámico del objeto,

(11.4) - un tipo que es del tipo firmado o no firmado correspondiente al tipo dinámico del objeto,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o elemento de datos no estático de un subaggregado o unión contenida),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - un tipo que es un tipo de clase base (posiblemente cv-calificado) del tipo dinámico del objeto,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - un carácter char, unsigned char, o std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Digno de notar signo firmado no está incluido en la lista anterior, esta es una diferencia notable de do que dice un tipo de personaje.

¿Qué es el tipo de Punning?

Hemos llegado a este punto y nos podemos estar preguntando, ¿por qué querríamos alias? La respuesta generalmente es tipo juego de palabras, a menudo los métodos utilizados violan reglas de aliasing estrictas.

A veces queremos eludir el sistema de tipos e interpretar un objeto como un tipo diferente. Se llama tipo de juego de palabras, para reinterpretar un segmento de memoria como otro tipo. Tipo de juego de palabras es útil para tareas que quieren acceder a la representación subyacente de un objeto para ver, transportar o manipular. Las áreas típicas en las que nos encontramos con el tipo de punteo que se usa son los compiladores, la serialización, el código de red, etc.

Tradicionalmente, esto se ha logrado tomando la dirección del objeto, convirtiéndolo en un puntero del tipo que queremos reinterpretar como y luego accediendo al valor, o en otras palabras, mediante alias. Por ejemplo:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( “%f\n”, *fp ) ;

Como hemos visto anteriormente, este no es un alias válido, por lo que estamos invocando un comportamiento indefinido. Pero tradicionalmente los compiladores no aprovechaban las estrictas reglas de aliasing y este tipo de código por lo general solo funcionaba, desafortunadamente los desarrolladores se han acostumbrado a hacer las cosas de esta manera. Un método alternativo común para el tipo de juego de palabras es a través de uniones, que es válido en C, pero comportamiento indefinido en C ++ (ver ejemplo en vivo)

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Esto no es válido en C ++ y algunos consideran que el propósito de las uniones es únicamente implementar tipos de variantes y sentir que el uso de sindicatos para el tipo de juego de palabras es un abuso.

¿Cómo escribimos Pun correctamente?

El método estándar para tipo de juego de palabras tanto en C como en C ++ es memcpy. Esto puede parecer un poco pesado, pero el optimizador debe reconocer el uso de memcpy para tipo de juego de palabras y optimizarlo y generar un registro para registrar movimiento. Por ejemplo, si sabemos int64_t es del mismo tamaño que doble:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

nosotros podemos usar memcpy:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

En un nivel de optimización suficiente, cualquier compilador moderno decente genera un código idéntico al anteriormente mencionado reinterpretar_cast método o Unión método para tipo de juego de palabras. Examinando el código generado vemos que usa simplemente registrar mov (Ejemplo de Live Compiler Explorer)

C ++ 20 y bit_cast

En C ++ 20 podemos ganar bit_cast que brinda una manera simple y segura de escribir y escribir, además de ser utilizable en un contexto de constexto.

El siguiente es un ejemplo de cómo usar bit_cast escribir un juego de palabras unsigned int a flotador, (verlo en vivo)

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

En el caso donde A y De los tipos no tienen el mismo tamaño, requiere que usemos una estructura intermedia15. Utilizaremos una estructura que contenga un sizeof (unsigned int) matriz de caracteres (asume 4 byte unsigned int) ser el Detipo y unsigned int como el A tipo.:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Es desafortunado que necesitemos este tipo intermedio pero esa es la restricción actual de bit_cast.

Captura de violaciones estrictas de aliasing

No contamos con muchas herramientas para capturar el alias estricto en C ++, las herramientas que tenemos detectarán algunos casos de violaciones estrictas de aliasing y algunos casos de cargas y tiendas mal alineadas.

gcc usando la bandera -flexible-aliasing y -Wstrict-aliasing puede detectar algunos casos, aunque no sin falsos positivos / negativos. Por ejemplo, los siguientes casos generarán una advertencia en gcc (verlo en vivo)

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

aunque no captará este caso adicional (verlo en vivo)

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Aunque clang permite estas banderas, aparentemente no implementa las advertencias.

Otra herramienta que tenemos disponible es ASan, que puede detectar cargas y tiendas desalineadas. Aunque estas no son violaciones de aliasing directamente estrictas, son un resultado común de violaciones estrictas de aliasing. Por ejemplo, los siguientes casos generarán errores de tiempo de ejecución cuando se construyan con clang usando -fsanitize = dirección

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

La última herramienta que recomendaré es específica de C ++ y no es estrictamente una herramienta, sino una práctica de codificación, no permite los moldes de estilo C. Tanto gcc como clang producirán un diagnóstico de moldes de estilo C usando -Wold-style-cast. Esto obligará a los juegos de palabras de tipo indefinido a usar reinterpret_cast, en general reinterpret_cast debería ser un indicador para una revisión más detallada del código. También es más fácil buscar su código base para reinterpret_cast para realizar una auditoría.

Para C tenemos todas las herramientas ya cubiertas y también tenemos tis-intérprete, un analizador estático que analiza exhaustivamente un programa para un gran subconjunto del lenguaje C. Dadas las veriones del ejemplo anterior donde se usa -flexible-aliasing falta un caso (verlo en vivo)

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter puede capturar los tres, el siguiente ejemplo invoca tis-kernal como tis-intérprete (la salida se edita para abreviar):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Finalmente hay TySan que está actualmente en desarrollo. Este desinfectante agrega información de verificación de tipo en un segmento de memoria oculta y comprueba los accesos para ver si violan las reglas de aliasing. La herramienta potencialmente debería poder detectar todas las violaciones de alias, pero puede tener una sobrecarga de tiempo de ejecución grande.


7
2017-09-19 01:33



El alias estricto no permite diferentes tipos de puntero a los mismos datos.

Este artículo debería ayudarlo a comprender el problema con todo detalle.


6
2017-07-09 03:24



Técnicamente en C ++, la regla de aliasing estricta probablemente nunca sea aplicable.

Tenga en cuenta la definición de indirección (* operador)

El operador unario * realiza indirección: la expresión a la que   se aplica debe ser un puntero a un tipo de objeto o un puntero a   tipo de función y el resultado es un lvalue refiriéndose al objeto o   función a lo que la expresión apunta.

También de la definición de glvalue

Un glvalue es una expresión cuya evaluación determina la identidad de   un objeto, (... snip)

Entonces, en cualquier rastro de programa bien definido, un glvalue se refiere a un objeto. Entonces, la llamada regla de aliasing estricta no se aplica, nunca. Esto puede no ser lo que los diseñadores querían.


-1