¿Está bien definido mantener un puntero desalineado, siempre y cuando no lo desviejes nunca?
pointers alignment (4)
Algunos compiladores pueden suponer que ningún puntero mantendrá un valor que no esté alineado correctamente para su tipo, y realizar optimizaciones basadas en eso. Como un simple ejemplo, considere:
void copy_uint32(uint32_t *dest, uint32_t *src)
{
memcpy(dest, src, sizeof (uint32_t));
}
Si tanto dest
como src
contienen direcciones alineadas de 32 bits, la función anterior podría optimizarse para una carga y una tienda incluso en plataformas que no admiten accesos no alineados. Sin embargo, si la función se hubiera declarado para aceptar argumentos de tipo void*
, dicha optimización no se permitiría en plataformas donde los accesos de 32 bits no alineados se comportarían de forma diferente a una secuencia de accesos de bytes, desplazamientos y operaciones de bits.
Tengo un código C que analiza los datos binarios empaquetados / sin relleno que vienen de la red.
Este código estaba / está funcionando bien en Intel / x86, pero cuando lo compilé bajo ARM a menudo se bloqueaba.
El culpable, como habrás adivinado, era punteros no alineados, en particular, el código de análisis haría cosas cuestionables como esta:
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]); // misaligned access -- can crash under ARM!
... obviamente no va a volar en ARM-land, así que lo modifiqué para que se pareciera más a esto:
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord)); // slower but ARM-safe
Mi pregunta (desde la perspectiva de un abogado de idiomas) es: ¿mi enfoque "ARM-fixed" está bien definido bajo las reglas de lenguaje C?
Mi preocupación es que quizás incluso tener un puntero desalineado-int32_t sea suficiente para invocar un comportamiento indefinido, incluso si en realidad nunca lo desreferenciamos directamente. (Si mi preocupación es válida, creo que podría solucionar el problema cambiando el tipo de (const int32_t *)
de (const int32_t *)
a (const char *)
, pero preferiría no hacerlo a menos que sea realmente necesario hacerlo, ya que significaría hacer algo de aritmética de zancadas con la mano)
Como se menciona en la respuesta de Antti Haapala, simplemente convertir un puntero a otro tipo cuando el puntero resultante no está alineado correctamente invoca un comportamiento indefinido según la sección 6.3.2.3p7 del estándar C.
Su código modificado solo usa pNextWord
para pasar a memcpy
, donde se convierte en un void *
, por lo que ni siquiera necesita una variable de tipo uint32_t *
. Simplemente pase la dirección del primer byte en el búfer que desea leer a memcpy
. Entonces no necesita preocuparse por la alineación en absoluto.
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));
No, el nuevo código todavía tiene un comportamiento indefinido. C11 6.3.2.3p7 :
- Un puntero a un tipo de objeto se puede convertir a un puntero a un tipo de objeto diferente. Si el puntero resultante no está alineado correctamente 68) para el tipo al que se hace referencia, el comportamiento no está definido. [...]
No dice nada sobre desreferenciar el puntero, incluso la conversión tiene un comportamiento indefinido.
De hecho, el código modificado que usted supone que es seguro para ARM puede no ser incluso seguro para Intel : se sabe que las compilaciones generan código para Intel que puede bloquearse en un acceso no alineado . Aunque no está en el caso vinculado, podría ser que un compilador inteligente pueda tomar la conversión como una prueba de que la dirección está efectivamente alineada y usar un código especializado para memcpy
.
Alineación a un lado, su primer extracto también sufre una violación estricta aliasing. C11 6.5p7 :
- Un objeto debe tener su valor almacenado al que se accede solo mediante una expresión lvalue que tiene uno de los siguientes tipos: 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 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
Dado que la matriz buf[2048]
está tipada estáticamente , cada elemento es char
, y por lo tanto los tipos efectivos de los elementos son char
; Puede acceder al contenido de la matriz solo como caracteres, no como int32_t
s.
Es decir, incluso
int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);
tiene un comportamiento indefinido
Para analizar con seguridad el número entero de varios bytes en compiladores / plataformas, puede extraer cada byte y ensamblarlos en enteros de acuerdo con endian. Por ejemplo, para leer un entero de 4 bytes del búfer big-endian:
uint8_t* buf = any address;
uint32_t val = 0;
uint32_t b0 = buf[0];
uint32_t b1 = buf[1];
uint32_t b2 = buf[2];
uint32_t b3 = buf[3];
val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;