plusplus plus documentacion cpp c++ c++11 language-lawyer

documentacion - c++ plusplus



Acceso no alineado a través de reinterpret_cast (3)

Mirando a 3.11 / 1:

Los tipos de objetos tienen requisitos de alineación (3.9.1, 3.9.2) que imponen restricciones en las direcciones a las que se puede asignar un objeto de ese tipo.

Hay cierto debate en los comentarios acerca de qué constituye exactamente la asignación de un objeto de un tipo. Sin embargo, creo que el siguiente argumento funciona independientemente de cómo se resuelva esa discusión:

Tome *reinterpret_cast<uint32_t*>(a) por ejemplo. Si esta expresión no causa UB, entonces (de acuerdo con la regla estricta de aliasing) debe haber un objeto de tipo uint32_t (o int32_t ) en la ubicación dada después de esta declaración. Si el objeto ya estaba allí, o esta escritura lo creó, no importa.

De acuerdo con la cotización estándar anterior, los objetos con requisitos de alineación solo pueden existir en un estado correctamente alineado.

Por lo tanto, cualquier intento de crear o escribir un objeto que no esté alineado correctamente causa UB.

Estoy en medio de una discusión tratando de averiguar si el acceso no alineado está permitido en C ++ a través de reinterpret_cast . Creo que no, pero estoy teniendo problemas para encontrar la (s) parte (s) correcta (s) de la norma que confirman o refutan eso. He estado buscando en C ++ 11, pero estaría bien con otra versión si es más claro.

El acceso no alineado no está definido en C11. La parte relevante de la norma C11 (§ 6.3.2.3, párrafo 7):

Un puntero a un tipo de objeto se puede convertir en un puntero a un tipo de objeto diferente. Si el puntero resultante no está alineado correctamente para el tipo referenciado, el comportamiento no está definido.

Dado que el comportamiento de un acceso no alineado no está definido, algunos compiladores (al menos GCC) consideran que está bien generar instrucciones que requieren datos alineados. La mayoría de las veces, el código aún funciona para datos no alineados porque la mayoría de las instrucciones x86 y ARM en estos días funcionan con datos no alineados, pero algunas no. En particular, algunas instrucciones vectoriales no lo hacen, lo que significa que a medida que el compilador mejora la generación de códigos de instrucciones optimizados que funcionaron con versiones anteriores, es posible que no funcionen con versiones más nuevas. Y, por supuesto, algunas arquitecturas ( como MIPS ) no funcionan tan bien con datos no alineados.

C++11 es, por supuesto, más complicado. § 5.2.10, el párrafo 7 dice:

Un puntero de objeto se puede convertir explícitamente en un puntero de objeto de un tipo diferente. Cuando un prvalue v de tipo "puntero a T1 " se convierte al tipo "puntero a cv T2 ", el resultado es static_cast<cv T2*>(static_cast<cv void*>(v)) si T1 y T2 son estándar -Los tipos de asignación (3.9) y los requisitos de alineación de T2 no son más estrictos que los de T1 , o si alguno de ellos es void . Convertir un prvalor de tipo "puntero a T1 " al tipo "puntero a T2 " (donde T1 y T2 son tipos de objeto y donde los requisitos de alineación de T2 no son más estrictos que los de T1 ) y volver a su tipo original produce el original valor del puntero El resultado de cualquier otra conversión de puntero no está especificado.

Tenga en cuenta que la última palabra es "no especificado", no "indefinido". § 1.3.25 define "comportamiento no especificado" como:

comportamiento, para una construcción de programa bien formada y datos correctos, que depende de la implementación

[ Nota : La implementación no es necesaria para documentar qué comportamiento ocurre. El rango de comportamientos posibles generalmente está delineado por esta Norma Internacional. - nota final ]

A menos que me esté faltando algo, el estándar no define realmente el rango de posibles comportamientos en este caso, lo que parece indicarme que un comportamiento muy razonable es el que se implementa para C (al menos por GCC): no es compatible ellos. Eso significaría que el compilador es libre de asumir que los accesos no alineados no ocurren y emiten instrucciones que pueden no funcionar con la memoria no alineada, tal como lo hace para C.

La persona con la que estoy discutiendo esto, sin embargo, tiene una interpretación diferente. Citan el § 1.9, párrafo 5:

Una implementación conforme que ejecute un programa bien formado producirá el mismo comportamiento observable que una de las posibles ejecuciones de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada. Sin embargo, si alguna ejecución de este tipo contiene una operación indefinida, esta Norma Internacional no impone ningún requisito a la implementación que ejecuta ese programa con esa entrada (ni siquiera con respecto a las operaciones que preceden a la primera operación no definida).

Dado que no hay un comportamiento indefinido , argumentan que el compilador de C ++ no tiene derecho a asumir que el acceso no alineado no ocurre.

Entonces, ¿son seguros los accesos no alineados a través de reinterpret_cast en C ++? ¿Dónde en la especificación (cualquier versión) dice?

Edición : Por "acceso", me refiero a cargar y almacenar. Algo como

void unaligned_cp(void* a, void* b) { *reinterpret_cast<volatile uint32_t*>(a) = *reinterpret_cast<volatile uint32_t*>(b); }

La forma en que se asigna la memoria está realmente fuera de mi alcance (es para una biblioteca a la que se puede llamar con datos desde cualquier lugar), pero es probable que malloc y una matriz en la pila sean candidatos. No quiero poner ninguna restricción sobre cómo se asigna la memoria.

Edición 2 : cite las fuentes ( es decir , el estándar de C ++, la sección y el párrafo) en las respuestas.


Todas sus citas se refieren al valor del puntero, no al acto de desreferenciación.

5.2.10, el párrafo 7 dice que, asumiendo que int tiene una alineación más estricta que char , entonces el viaje de ida y vuelta de char* a int* to char* genera un valor no especificado para el char* resultante char* .

Por otro lado, si convierte int* en char* en int* , está garantizado que obtendrá exactamente el mismo puntero con el que comenzó.

No se habla de lo que se obtiene cuando no se hace referencia a dicho indicador. Simplemente establece que, en un caso, debe ser capaz de realizar un viaje de ida y vuelta. Se lava las manos del revés.

Supongamos que tiene algunas alignof(int) > 1 y alignof(int) > 1 :

int some_ints[3] ={0};

entonces tienes un puntero int que está desplazado:

int* some_ptr = (int*)(((char*)&some_ints[0])+1);

Supondremos que copiar este puntero desalineado no causa un comportamiento indefinido por ahora.

El valor de some_ptr no está especificado por el estándar. Seremos generosos y supondremos que en realidad apunta a algunos fragmentos de bytes dentro de some_bytes .

Ahora tenemos un int* que apunta a un lugar donde no se puede asignar un int (3.11 / 1). En (3.8) el uso de un puntero a un int está restringido de varias maneras. El uso habitual está restringido a un puntero a una T cuya vida útil se ha asignado correctamente (/ 3). Se permite cierto uso limitado en un puntero a una T que se ha asignado correctamente, pero cuya vida útil no ha comenzado (/ 5 y / 6).

No hay forma de crear un objeto int que no cumpla con las restricciones de alineación de int en el estándar.

Por lo tanto, la int* teórica que pretende señalar una int desalineada no apunta a una int . No se colocan restricciones sobre el comportamiento de dicho puntero cuando no se hace referencia; las reglas de eliminación de referencias habituales proporcionan el comportamiento de un puntero válido a un objeto (incluido un int ) y su comportamiento.

Y ahora nuestros otros supuestos. El estándar no establece restricciones en el valor de some_ptr : int* some_ptr = (int*)(((char*)&some_ints[0])+1); .

No es un puntero a un int , al igual que (int*)nullptr no es un puntero a un int . El hecho de redondearlo de nuevo a un char* da como resultado un puntero con un valor no especificado (podría ser 0xbaadf00d o nullptr ) explícitamente en el estándar.

El estándar define lo que debes hacer. Hay (casi? Supongo que evaluarlo en un contexto booleano debe devolver un bool) no hay requisitos para el comportamiento de some_ptr por el estándar, aparte de convertirlo de nuevo a char* da como resultado un valor no especificado (del puntero).


EDITAR Responde a la pregunta original del OP, que fue "es acceder a un puntero desalineado seguro". Desde entonces, el OP ha editado su pregunta a "está haciendo referencia a la seguridad de un puntero desalineado", una pregunta mucho más práctica y menos interesante.

El resultado de conversión de ida y vuelta del valor del puntero no se especifica en esas circunstancias. Bajo ciertas circunstancias limitadas (que involucran alineación), convertir un puntero a A a un puntero a B, y luego de nuevo , genera el puntero original, incluso si no tenía una B en esa ubicación .

Si no se cumplen los requisitos de alineación, entonces ese viaje de ida y vuelta: el puntero a A al puntero a B al puntero a A da como resultado un puntero con un valor no especificado .

Como hay valores de puntero no válidos, la anulación de la referencia de un puntero con un valor no especificado puede dar como resultado un comportamiento indefinido. No es diferente de *(int*)0xDEADBEEF en cierto sentido.

Sin embargo, simplemente almacenar ese puntero no es un comportamiento indefinido.

Ninguna de las citas de C ++ anteriores habla sobre el hecho de usar un puntero a A como puntero a B. El uso de un puntero al "tipo incorrecto" en todas las circunstancias, salvo en un número muy limitado, es un comportamiento indefinido, punto.

Un ejemplo de esto implica crear un std::aligned_storage_t<sizeof(T), alignof(T)> . Puede construir su T en ese lugar, y vivirá felizmente, aunque "en realidad" sea aligned_storage_t<sizeof(T), alignof(T)> . (Sin embargo, es posible que tenga que usar el puntero devuelto de la ubicación new para cumplir con todos los requisitos; no estoy seguro. Consulte el alias estricto).

Lamentablemente, el estándar carece un poco en términos de lo que es la vida útil del objeto. Se refiere a él, pero no lo define lo suficientemente bien la última vez que lo verifiqué. Solo puede usar una T en una ubicación particular mientras que una T vive allí, pero lo que eso significa no está claro en todas las circunstancias.