En C, el compilador presentará los miembros de una estructura en el orden en que se declaran, con posibles bytes de relleno insertados entre los miembros, o después del último miembro, para garantizar que cada miembro esté alineado correctamente.
gcc proporciona una extensión de lenguaje __attribute__((packed))
, que le dice al compilador que no inserte relleno, permitiendo que los miembros de la estructura estén desalineados. Por ejemplo, si el sistema normalmente requiere que todos los int
objetos tengan una alineación de 4 bytes, __attribute__((packed))
puede hacer que los int
miembros de la estructura se asignen en desplazamientos impares.
Citando la documentación de gcc:
El atributo 'empaquetado' especifica que un campo de variable o estructura debe tener la alineación más pequeña posible: un byte para una variable y un bit para un campo, a menos que especifique un valor mayor con el atributo 'alineado'.
Obviamente, el uso de esta extensión puede dar como resultado requisitos de datos más pequeños pero un código más lento, ya que el compilador debe (en algunas plataformas) generar código para acceder a un miembro desalineado, un byte a la vez.
Pero, ¿hay casos en que esto no sea seguro? ¿El compilador siempre genera código correcto (aunque más lento) para acceder a miembros desalineados de estructuras empaquetadas? ¿Es posible que lo haga en todos los casos?
fuente
Respuestas:
Sí,
__attribute__((packed))
es potencialmente inseguro en algunos sistemas. El síntoma probablemente no aparecerá en un x86, lo que hace que el problema sea más insidioso; Las pruebas en sistemas x86 no revelarán el problema. (En el x86, los accesos desalineados se manejan en el hardware; si desreferencia unint*
puntero que apunta a una dirección impar, será un poco más lento que si estuviera correctamente alineado, pero obtendrá el resultado correcto).En algunos otros sistemas, como SPARC, intentar acceder a un
int
objeto desalineado provoca un error de bus que bloquea el programa.También ha habido sistemas en los que un acceso desalineado ignora silenciosamente los bits de orden inferior de la dirección, lo que hace que acceda al fragmento de memoria incorrecto.
Considere el siguiente programa:
En Ubuntu x86 con gcc 4.5.2, produce el siguiente resultado:
En SPARC Solaris 9 con gcc 4.5.1, produce lo siguiente:
En ambos casos, el programa se compila sin opciones adicionales, solo
gcc packed.c -o packed
.(Un programa que utiliza una sola estructura en lugar de una matriz no presenta el problema de manera confiable, ya que el compilador puede asignar la estructura en una dirección impar para que el
x
miembro esté alineado correctamente. Con una matriz de dosstruct foo
objetos, al menos uno u otro tendrá unx
miembro desalineado ).(En este caso,
p0
apunta a una dirección desalineada, porque apunta a unint
miembro empaquetado que sigue a unchar
miembro.p1
Está alineado correctamente, ya que apunta al mismo miembro en el segundo elemento de la matriz, por lo que hay doschar
objetos que lo preceden - y en SPARC Solaris, la matrizarr
parece estar asignada a una dirección que es par, pero no un múltiplo de 4.)Al referirse al miembro
x
de astruct foo
por nombre, el compilador sabe quex
está potencialmente desalineado y generará código adicional para acceder a él correctamente.Una vez que la dirección de
arr[0].x
oarr[1].x
se ha almacenado en un objeto puntero, ni el compilador ni el programa en ejecución saben que apunta a unint
objeto desalineado . Simplemente se supone que está alineado correctamente, lo que resulta (en algunos sistemas) en un error del bus u otra falla similar.Arreglar esto en gcc sería, en mi opinión, poco práctico. Una solución general requeriría, para cada intento de desreferenciar un puntero a cualquier tipo con requisitos de alineación no triviales, ya sea (a) probar en tiempo de compilación que el puntero no apunta a un miembro desalineado de una estructura empaquetada, o (b) generar código más voluminoso y lento que puede manejar objetos alineados o desalineados.
He enviado un informe de error de gcc . Como dije, no creo que sea práctico arreglarlo, pero la documentación debería mencionarlo (actualmente no lo hace).
ACTUALIZACIÓN : A partir del 2018-12-20, este error está marcado como FIJO. El parche aparecerá en gcc 9 con la adición de una nueva
-Waddress-of-packed-member
opción, habilitada por defecto.Acabo de construir esa versión de gcc desde la fuente. Para el programa anterior, produce estos diagnósticos:
fuente
Como ams dijo anteriormente, no lleve un puntero a un miembro de una estructura que esté empaquetada. Esto es simplemente jugar con fuego. Cuando dices
__attribute__((__packed__))
o#pragma pack(1)
, lo que realmente estás diciendo es "Hola gcc, realmente sé lo que estoy haciendo". Cuando resulta que no lo hace, no puede culpar correctamente al compilador.Sin embargo, quizás podamos culpar al compilador por su complacencia. Si bien gcc tiene una
-Wcast-align
opción, no está habilitada de manera predeterminada ni con-Wall
o-Wextra
. Aparentemente, esto se debe a que los desarrolladores de gcc consideran que este tipo de código es una " abominación " mortal que no merece ser abordada, un desdén comprensible, pero no ayuda cuando un programador inexperto se topa con él.Considera lo siguiente:
Aquí, el tipo de
a
es una estructura empaquetada (como se definió anteriormente). Del mismo modo,b
es un puntero a una estructura empaquetada. El tipo de expresióna.i
es (básicamente) un valor int l con alineación de 1 byte.c
yd
ambos son normalesint
s. Al leera.i
, el compilador genera código para acceso no alineado. Cuando leesb->i
,b
el tipo todavía sabe que está empaquetado, así que no hay problema tampoco.e
es un puntero a un int alineado en un byte, por lo que el compilador también sabe cómo desreferenciarlo correctamente. Pero cuando realiza la asignaciónf = &a.i
, está almacenando el valor de un puntero int no alineado en una variable de puntero int alineado; ahí es donde se equivocó. Y estoy de acuerdo, gcc debería tener esta advertencia habilitada porpredeterminado (ni siquiera en-Wall
o-Wextra
).fuente
__attribute__((aligned(1)))
es una extensión gcc y no es portátil. Que yo sepa, la única forma realmente portátil de hacer acceso no alineado en C (con cualquier combinación de compilador / hardware) es con una copia de memoria en bytes (memcpy o similar). Algunos hardware ni siquiera tienen instrucciones para el acceso no alineado. Mi experiencia es con arm y x86 que pueden hacer ambas cosas, aunque el acceso no alineado es más lento. Entonces, si alguna vez necesita hacer esto con un alto rendimiento, deberá oler el hardware y usar trucos específicos para el arco.__attribute__((aligned(x)))
ahora parece ser ignorado cuando se usa para punteros. :( Todavía no tengo los detalles completos de esto, pero el uso__builtin_assume_aligned(ptr, align)
parece obtener gcc para generar el código correcto. Cuando tenga una respuesta más concisa (y con suerte un informe de error) actualizaré mi respuesta.uint32_t
miembro generará unuint32_t packed*
; tratar de leer desde dicho puntero en, por ejemplo, un Cortex-M0 IIRC llamará a una subrutina que tomará ~ 7 veces más que una lectura normal si el puntero no está alineado o ~ 3 veces más si está alineado, pero se comportará de manera predecible en cualquier caso [el código en línea tardaría 5 veces más si está alineado o no]Es perfectamente seguro siempre que acceda a los valores a través de la estructura a través del
.
(punto) o la->
notación.Lo que no es seguro es tomar el puntero de los datos no alineados y luego acceder a ellos sin tenerlo en cuenta.
Además, aunque se sabe que cada elemento de la estructura no está alineado, se sabe que no está alineado de una manera particular , por lo que la estructura en su conjunto debe estar alineada como el compilador espera o habrá problemas (en algunas plataformas, o en el futuro si se inventa una nueva forma de optimizar los accesos no alineados).
fuente
Usar este atributo definitivamente no es seguro.
Una cosa particular que rompe es la capacidad de un
union
que contiene dos o más estructuras para escribir un miembro y leer otro si las estructuras tienen una secuencia inicial común de miembros. La sección 6.5.2.3 de los estados estándar de C11 :Cuando
__attribute__((packed))
se introduce se rompe esto. El siguiente ejemplo se ejecutó en Ubuntu 16.04 x64 usando gcc 5.4.0 con optimizaciones deshabilitadas:Salida:
A pesar de que
struct s1
ystruct s2
tener una "secuencia inicial común", el embalaje aplicada a los primero significa que los miembros correspondientes no vivo en el desplazamiento del mismo byte. El resultado es que el valor escrito en memberx.b
no es el mismo que el valor leído de membery.b
, aunque el estándar dice que deberían ser iguales.fuente
(El siguiente es un ejemplo muy artificial preparado para ilustrar). Un uso importante de las estructuras empaquetadas es donde tiene una secuencia de datos (digamos 256 bytes) a los que desea proporcionar significado. Si tomo un ejemplo más pequeño, supongamos que tengo un programa ejecutándose en mi Arduino que envía vía serial un paquete de 16 bytes que tiene el siguiente significado:
Entonces puedo declarar algo como
y luego puedo referirme a los bytes targetAddr a través de aStruct.targetAddr en lugar de jugar con la aritmética del puntero.
Ahora que ocurre la alineación, tomar un puntero nulo * en la memoria a los datos recibidos y enviarlo a myStruct * no funcionará a menos que el compilador trate la estructura como empaquetada (es decir, almacena los datos en el orden especificado y usa exactamente 16 bytes para este ejemplo). Existen penalizaciones de rendimiento para lecturas no alineadas, por lo que usar estructuras empaquetadas para los datos con los que su programa está trabajando activamente no es necesariamente una buena idea. Pero cuando su programa recibe una lista de bytes, las estructuras empaquetadas facilitan la escritura de programas que acceden a los contenidos.
De lo contrario, terminas usando C ++ y escribiendo una clase con métodos de acceso y cosas que hacen aritmética de punteros detrás de escena. En resumen, las estructuras empaquetadas son para tratar de manera eficiente los datos empaquetados, y los datos empaquetados pueden ser lo que su programa tiene para trabajar. En su mayor parte, el código debe leer los valores fuera de la estructura, trabajar con ellos y escribirlos cuando haya terminado. Todo lo demás debe hacerse fuera de la estructura empaquetada. Parte del problema son las cosas de bajo nivel que C intenta ocultar al programador, y el salto de aro que se necesita si tales cosas realmente le importan al programador. (Casi necesita una construcción diferente de 'diseño de datos' en el lenguaje para poder decir 'esta cosa tiene 48 bytes de largo, foo se refiere a los datos de 13 bytes y debe interpretarse así'; y una construcción de datos estructurados por separado,
fuente