Pregunta

Al preguntar sobre comportamiento indefinido común en C, almas más ilustradas que yo se refirieron a la estricta regla del alias.
De qué están hablando?

¿Fue útil?

Solución

Una situación típica en la que se encuentran problemas estrictos de alias es cuando se superpone una estructura (como un mensaje de 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 mediante la conversión de punteros, puede violar fácilmente reglas estrictas de alias.

Entonces, en este tipo de configuración, si quiero enviar un mensaje a algo, tendría que tener dos punteros incompatibles que apunten al mismo fragmento de memoria.Entonces podría codificar ingenuamente 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 estricta regla de alias hace que esta configuración sea ilegal:desreferenciar un puntero que alias un objeto que no es de un 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, haz que se compile bien, solo para tener un comportamiento extraño e inesperado cuando ejecutas el código.

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

Para ver por qué este comportamiento no está definido, tenemos que pensar en lo que la estricta regla de alias compra al compilador.Básicamente, con esta regla, no es necesario pensar en insertar instrucciones para actualizar el contenido de buff cada ejecución del bucle.En cambio, al optimizar, con algunas suposiciones molestas y no aplicadas sobre el alias, puede omitir esas instrucciones, cargar buff[0] y buff[1] en los registros de la CPU una vez antes de ejecutar el bucle y acelerar el cuerpo del bucle.Antes de que se introdujera el alias estricto, el compilador tenía que vivir en un estado de paranoia de que el contenido de buff podría cambiar en cualquier momento y desde cualquier lugar por cualquiera.Entonces, para obtener una ventaja adicional en el rendimiento, y asumiendo que la mayoría de las personas no escriben punteros, se introdujo la estricta regla de alias.

Tenga en cuenta que si cree que el ejemplo es artificial, esto podría suceder incluso si está pasando un búfer a otra función que realiza el envío por usted, si es que así lo ha hecho.

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

Y reescribimos nuestro bucle 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 capaz o lo suficientemente inteligente como para intentar integrar SendMessage y puede decidir o no cargar o no buff nuevamente.Si SendMessage es parte de otra API que se compila por separado, probablemente tenga instrucciones para cargar el contenido de buff.Por otra parte, tal vez estés en C++ y esta sea una implementación de encabezado con plantilla que el compilador cree que puede incorporar.O tal vez sea simplemente algo que escribiste en tu archivo .c para tu propia conveniencia.De todos modos, aún podría producirse un comportamiento indefinido.Incluso cuando sabemos algo de lo que sucede bajo el capó, sigue siendo una violación de la regla, por lo que no se garantiza ningún comportamiento bien definido.Entonces, simplemente incluir una función que tome nuestro búfer delimitado por palabras no necesariamente ayuda.

Entonces, ¿cómo soluciono esto?

  • Utilice una unión.La mayoría de los compiladores admiten esto sin quejarse del uso de alias estricto.Esto está permitido en C99 y explícitamente en C11.

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

  • Puedes usar char* para alias en lugar de la palabra de su sistema.Las reglas permiten una excepción para char* (incluido signed char y unsigned char).Siempre se supone que char* alias otros tipos.Sin embargo, esto no funcionará al revés:No se supone que su estructura tenga un alias de búfer de caracteres.

Cuidado principiante

Este es sólo un campo minado potencial cuando se superponen dos tipos entre sí.También deberías aprender sobre endianidad, alineación de palabras, y cómo abordar los problemas de alineación mediante estructuras de embalaje correctamente.

Nota

1 Los tipos a los que C 2011 6.5 7 permite que acceda un valor l 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 sin firmar 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 tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión subagregada o contenida), o
  • un tipo de personaje.

Otros consejos

La mejor explicación que he encontrado es la de Mike Acton, Comprender el alias estricto.Se centra un poco en el desarrollo de PS3, pero básicamente es sólo GCC.

Del artículo:

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

Básicamente, si tienes un int* apuntando a algún recuerdo que contiene un int y luego señalas un float* a ese recuerdo y usarlo como float rompes la regla.Si su código no respeta esto, lo más probable es que el optimizador del compilador rompa su código.

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

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

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

  • el tipo dinámico del objeto,
  • una versión calificada por CV del tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada por cv del tipo dinámico 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 unión subagregada o contenida),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,
  • a char o unsigned char tipo.

C++11 y C++14 redacción (se enfatizan los cambios):

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

  • el tipo dinámico del objeto,
  • una versión calificada por CV 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 sin firmar correspondiente al tipo dinámico del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada por cv del tipo dinámico del objeto,
  • un tipo agregado o unión que incluye uno de los tipos antes mencionados entre sus elementos o miembros de datos no estáticos (incluyendo, recursivamente, un elemento o miembro de datos no estáticos de una unión subagregada o contenida),
  • un tipo que es un tipo de clase base (posiblemente calificado por cv) del tipo dinámico del objeto,
  • a char o unsigned char tipo.

Dos cambios fueron pequeños: valor gl en lugar de valor l, y aclaración del caso agregado/sindicato.

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


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

Un objeto tendrá su valor almacenado accedido solo por una expresión de Lvalue que tenga uno de los siguientes tipos 73) o 88):

  • 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 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 tipo agregado o sindicato que incluye uno de los tipos mencionados entre sus miembros (incluidos, recursivamente, un miembro de una unión subaggregada o contenida), o
  • un tipo de personaje.

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

Nota

Esto es un extracto de mi "¿Qué es la regla estricta de alias y por qué nos importa?" redacción.

¿Qué es el alias estricto?

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

Desafortunadamente, con violaciones estrictas 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 pensábamos que era válido.Esto no es deseable y vale la pena comprender las estrictas reglas de alias y cómo evitar violarlas.

Para comprender más acerca de por qué nos importa, analizaremos los problemas que surgen al violar reglas estrictas de alias, los juegos de palabras tipográficos, ya que las técnicas comunes utilizadas en los juegos de palabras tipográficos a menudo violan las reglas estrictas de aliasing y cómo escribir juegos de palabras correctamente.

Ejemplos preliminares

Veamos algunos ejemplos, luego podremos hablar exactamente de lo que dicen los estándares, examinar algunos ejemplos adicionales y luego ver cómo evitar el uso de alias estricto y detectar infracciones que pasamos por alto.He aquí un ejemplo que no debería sorprender (ejemplo 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 alias válido.El optimizador debe asumir que las asignaciones a través de IP podría actualizar el valor ocupado por X.

El siguiente ejemplo muestra el alias que conduce a un comportamiento indefinido (ejemplo 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 funcion foo tomamos un En t* y un flotar*, en este ejemplo llamamos foo y configure 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 flotar*.Podemos esperar ingenuamente el resultado de la segunda corte ser 0 pero con la optimización habilitada usando -O2 Tanto gcc como clang producen el siguiente resultado:

0
1

Lo cual puede que no sea de esperar, pero es perfectamente válido ya que hemos invocado un comportamiento indefinido.A flotar no puede alias válidamente un En t objeto.Por lo tanto, el optimizador puede asumir la constante 1 almacenado al desreferenciar i será el valor de retorno desde una tienda hasta F no podría afectar válidamente a una En t objeto.Al conectar el código en Compiler Explorer se muestra que esto es exactamente lo que está sucediendo (ejemplo 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 tipos (TBAA) asume 1 será devuelto y mueve directamente el valor constante al registro eax que lleva el valor de retorno.TBAA utiliza las reglas de los idiomas sobre qué tipos pueden tener alias para optimizar cargas y almacenes.En este caso TBAA sabe que un flotar no puede alias y En t y optimiza la carga de i.

Ahora, al libro de reglas

¿Qué dice exactamente el estándar que podemos y no podemos hacer?El lenguaje estándar no es sencillo, por lo que para cada elemento intentaré proporcionar ejemplos de código que demuestren el significado.

¿Qué dice la norma C11?

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

A un objeto se le podrá acceder a su valor almacenado únicamente 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 cualificada de un tipo compatible con el tipo efectivo del objeto,

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 sea el tipo firmado o sin firmar 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 también que permite asignar entero sin firmar* a En t* aunque no sean 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 antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión subagregada o contenida), 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.

Lo que dice el borrador del estándar C++17

El borrador del estándar 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 que no sea 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 calificada cv 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 el tipo con o sin signo 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 calificada cv 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 miembro de datos no estáticos de un subagregado 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 calificado por cv) 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 tipo 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 mención carácter firmado no está incluido en la lista anterior, esta es una diferencia notable con respecto a C que dice un tipo de personaje.

¿Qué es el juego de palabras?

Hemos llegado a este punto y es posible que nos preguntemos: ¿por qué querríamos utilizar un alias?La respuesta normalmente es tipo juego de palabras, a menudo los métodos utilizados violan estrictas reglas de alias.

A veces queremos eludir el sistema de tipos e interpretar un objeto como un tipo diferente.Se llama tipo juego de palabras, para reinterpretar un segmento de memoria como otro tipo. Tipo juego de palabras Es útil para tareas que desean acceder a la representación subyacente de un objeto para verlo, transportarlo o manipularlo.Las áreas típicas en las que encontramos que se utilizan juegos de palabras son compiladores, serialización, código de red, etc.

Tradicionalmente, esto se ha logrado tomando la dirección del objeto, convirtiéndola en un puntero del tipo que queremos reinterpretarlo 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 invocamos un comportamiento indefinido.Pero tradicionalmente los compiladores no aprovechaban las reglas estrictas de alias y este tipo de código generalmente simplemente funcionaba; desafortunadamente, los desarrolladores se han acostumbrado a hacer las cosas de esta manera.Un método alternativo común para los juegos de palabras es a través de uniones, que es válida 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 variantes y sienten que usar uniones para juegos de palabras es un abuso.

¿Cómo escribimos Pun correctamente?

El método estándar para tipo juego de palabras tanto en C como en C++ es memcpy.Esto puede parecer un poco complicado, pero el optimizador debería reconocer el uso de memcpy para tipo juego de palabras y optimizarlo y generar un registro para registrar el 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); 
  //...

Con un nivel de optimización suficiente, cualquier compilador moderno decente genera código idéntico al mencionado anteriormente. reinterpretar_cast método o Unión método para tipo juego de palabras.Al examinar el código generado, vemos que usa solo registrar mov (Ejemplo de Explorador de compiladores en vivo).

C++20 y bit_cast

En C++20 podemos ganar bit_cast (implementación disponible en el enlace de la propuesta) que proporciona una forma sencilla y segura de escribir juegos de palabras, además de ser utilizable en un contexto constexpr.

El siguiente es un ejemplo de cómo utilizar bit_cast escribir un juego de palabras entero sin firmar a flotar, (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.Usaremos una estructura que contiene un tamaño de (int sin firmar) matriz de caracteres (asume 4 bytes int sin signo) ser el De tipo y entero sin firmar 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 lamentable que necesitemos este tipo intermedio, pero esa es la limitación actual de bit_cast.

Detectar infracciones estrictas de alias

No tenemos muchas herramientas buenas para detectar alias estrictos en C++, las herramientas que tenemos detectarán algunos casos de violaciones de alias estrictas y algunos casos de cargas y almacenes desalineados.

gcc usando la bandera -fstrict-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 detectará este caso adicional (verlo en vivo):

int *p;

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

Aunque clang permite estos indicadores, aparentemente en realidad no implementa las advertencias.

Otra herramienta que tenemos disponible es ASan, que puede detectar cargas y almacenes desalineados.Aunque estas no son directamente violaciones de alias estrictas, son un resultado común de violaciones de alias estrictas.Por ejemplo, los siguientes casos generarán errores de tiempo de ejecución cuando se compilan 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 permita conversiones de estilo C.Tanto gcc como clang producirán un diagnóstico para los modelos estilo C utilizando -Elenco al estilo Wold.Esto obligará a cualquier juego de palabras de tipo indefinido a utilizar reinterpret_cast; en general, reinterpret_cast debería ser una señal para una revisión más detallada del código.También es más fácil buscar en su código base reinterpret_cast para realizar una auditoría.

Para C tenemos todas las herramientas ya cubiertas y también tenemos tis-interpreter, un analizador estático que analiza exhaustivamente un programa para un gran subconjunto del lenguaje C.Dadas las versiones C del ejemplo anterior donde se usa -fstrict-aliasing pierde 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 a tis-kernal como tis-interpreter (la salida se edita para mayor brevedad):

./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 actualmente se encuentra en desarrollo.Este desinfectante agrega información de verificación de tipos en un segmento de memoria oculta y verifica los accesos para ver si violan las reglas de alias.Potencialmente, la herramienta debería poder detectar todas las infracciones de alias, pero puede tener una gran sobrecarga de tiempo de ejecución.

El alias estricto no se refiere sólo a punteros, afecta también a las referencias. Escribí un artículo al respecto para la wiki de desarrolladores de Boost 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. Informe técnico sobre alias estricto.En particular, explica por qué las uniones son un comportamiento riesgoso para C++ y por qué usar memcpy es la única solución portátil tanto en C como en C++.Espero que esto sea útil.

Como complemento a lo que Doug T.Ya escribí, aquí hay un caso de prueba simple que probablemente lo desencadena con GCC:

comprobar.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 .Por lo general (con la mayoría de las versiones de gcc que probé) esto genera un "problema de alias estricto", porque el compilador supone que "h" no puede ser la misma dirección que "k" en la función "verificar".Por eso el compilador optimiza el 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, que se ejecuta en ubuntu 12.04.2 para x64:

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

Entonces, la condición if desapareció por completo del código ensamblador.

Tipo juego de palabras mediante conversiones de punteros (en lugar de usar una unión) es un ejemplo importante de cómo romper el alias estricto.

Según el fundamento del C89, los autores del Estándar no querían exigir que los compiladores proporcionaran código como:

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

debe ser requerido para recargar el valor de x entre la cesión y la declaración de devolución para permitir la posibilidad de que p podría señalar a x, y la asignación a *p en consecuencia podría alterar el valor de x.La noción de que un compilador debería tener derecho a suponer que no habrá alias en situaciones como las anteriores no fue controvertido.

Desafortunadamente, los autores del C89 escribieron su regla de una manera que, si se lee literalmente, haría que incluso la siguiente función invoque un comportamiento indefinido:

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

porque usa un valor l de tipo int para acceder a un objeto de tipo struct S, y int no se encuentra entre los tipos que se pueden utilizar para acceder a un struct S.Debido a que sería absurdo tratar todo uso de miembros de estructuras y uniones que no sean de tipo carácter como comportamiento indefinido, casi todos reconocen que existen al menos algunas circunstancias en las que un valor l de un tipo puede usarse para acceder a un objeto de otro tipo. .Lamentablemente, el Comité de Normas C no ha definido cuáles son esas circunstancias.

Gran parte del problema es el resultado del Informe de defectos n.º 028, que preguntaba 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 un comportamiento indefinido porque la acción de escribir un miembro de unión de tipo "double" y leer uno de tipo "int" invoca un comportamiento definido por la implementación.Tal razonamiento no tiene sentido, pero constituye la base de las reglas de tipo efectivo que complican innecesariamente el lenguaje sin hacer nada para abordar el problema original.

La mejor manera de resolver el problema original probablemente sería tratar la nota al pie del propósito de la regla como si fuera normativa, e hizo que la regla fuera inaplicable, excepto en casos que realmente implican accesos en conflicto con 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 al que se accede a través *p se hacen con un valor l de tipo int, y no hay conflicto en test porque p se deriva visiblemente de una struct S, y para la próxima vez s se utiliza, todos los accesos a ese almacenamiento que alguna vez se realicen a través p ya habrá sucedido.

Si el código se cambiara ligeramente...

 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 alias entre p y el acceso a s.x en la línea marcada porque en ese punto de la ejecución existe otra referencia que se utilizará para acceder al mismo almacenamiento.

Si el Informe de defectos 028 hubiera dicho 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.

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

Alias ​​estricto (que describiré más adelante) es importante porque:

  1. El acceso a la memoria puede ser costoso (en términos de rendimiento), razón por la cual los datos se manipulan en los registros de la CPU antes de volver a escribirse 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 ensamblador, donde codificamos la carga y descarga de registros de la CPU manualmente, sabremos qué datos permanecen intactos.Pero C (afortunadamente) abstrae este detalle.

Dado que 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 adicional es lento y perjudica el rendimiento ya que realiza operaciones de lectura/escritura de memoria adicional que son más lentas y (posiblemente) innecesarias.

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

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

Si un compilador nota que dos punteros apuntan a tipos diferentes (por ejemplo, un int * y un float *), asumirá que la dirección de memoria es diferente y no lo hará protege 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 atender el caso en el que a == b (ambos punteros apuntan a la misma memoria), necesitamos ordenar y probar la forma en que cargamos datos desde la memoria a los registros de la CPU, por lo que el código podría terminar así:

  1. carga a y b de memoria.

  2. agregar a a b.

  3. ahorrar 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. agregar b a a.

  5. ahorrar 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, es necesario para proteger contra casos en los que a y b apuntar a la misma dirección de memoria.

Un alias estricto nos permitiría evitar esto diciéndole 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 le puede decir al compilador de dos maneras, usando diferentes tipos a los que apuntar.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 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 el restrict palabra clave, toda la función podría optimizarse para:

  1. carga a y b de memoria.

  2. agregar a a b.

  3. guardar el resultado tanto en a y para b.

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

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

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

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

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

El operador unario * realiza dirección indirecta:La expresión a la que se aplica será un puntero a un tipo de objeto, o un puntero a un tipo de función y el resultado es un valor l que se refiere al objeto o función a lo que apunta la expresión.

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 seguimiento de programa bien definido, un valor gl se refiere a un objeto. Por lo tanto, la llamada regla estricta de alias nunca se aplica. Puede que esto no sea lo que querían los diseñadores.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top