¿El paquete __attribute__((packed)) / #pragma de gcc no es seguro?
En C, el compilador distribuirá 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 indica al compilador que no inserte relleno, lo que permite que los miembros de la estructura queden desalineados. Por ejemplo, si el sistema normalmente requiere que todos int
los objetos tengan una alineación de 4 bytes, __attribute__((packed))
puede provocar int
que los miembros de la estructura se asignen en desplazamientos impares.
Citando la documentación de gcc:
El atributo "empaquetado" especifica que una variable o campo de 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 menores requisitos de datos pero un código más lento, ya que el compilador debe (en algunas plataformas) generar código para acceder a un miembro desalineado byte a byte.
Pero, ¿hay algún caso en el 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 siquiera posible que lo haga en todos los casos?
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 hardware; si elimina la referencia a un int*
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 provoca que se acceda a una porción de memoria incorrecta.
Considere el siguiente programa:
#include <stdio.h>
#include <stddef.h>
int main(void)
{
struct foo {
char c;
int x;
} __attribute__((packed));
struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
int *p0 = &arr[0].x;
int *p1 = &arr[1].x;
printf("sizeof(struct foo) = %d\n", (int)sizeof(struct foo));
printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
printf("arr[0].x = %d\n", arr[0].x);
printf("arr[1].x = %d\n", arr[1].x);
printf("p0 = %p\n", (void*)p0);
printf("p1 = %p\n", (void*)p1);
printf("*p0 = %d\n", *p0);
printf("*p1 = %d\n", *p1);
return 0;
}
En Ubuntu x86 con gcc 4.5.2, produce el siguiente resultado:
sizeof(struct foo) = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20
En SPARC Solaris 9 con gcc 4.5.1, produce lo siguiente:
sizeof(struct foo) = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error
En ambos casos, el programa se compila sin opciones extra, sólo gcc packed.c -o packed
.
(Un programa que utiliza una única 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é correctamente alineado. Con una matriz de dos struct foo
objetos, al menos uno u otro tendrá un x
miembro desalineado.)
(En este caso, p0
apunta a una dirección desalineada, porque apunta a un int
miembro empaquetado que sigue a un char
miembro. p1
Está correctamente alineado, ya que apunta al mismo miembro en el segundo elemento de la matriz, por lo que hay dos char
objetos precediéndolo. -- y en SPARC Solaris la matriz arr
parece estar asignada en una dirección par, pero no múltiplo de 4.)
Al referirse al miembro x
de un struct foo
por su nombre, el compilador sabe que x
está potencialmente desalineado y generará código adicional para acceder a él correctamente.
Una vez que la dirección de arr[0].x
o arr[1].x
se ha almacenado en un objeto puntero, ni el compilador ni el programa en ejecución saben que apunta a un int
objeto desalineado. Simplemente supone que está alineado correctamente, lo que resulta (en algunos sistemas) en un error de bus u otra falla similar.
Creo que arreglar esto en gcc no sería 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, (a) demostrar en tiempo de compilación que el puntero no apunta a un miembro desalineado de una estructura empaquetada, o (b) generando código más voluminoso y lento que puede manejar objetos alineados o desalineados.
Envié un informe de error de gcc . Como dije, no creo que sea práctico solucionarlo, pero la documentación debería mencionarlo (actualmente no lo hace).
ACTUALIZACIÓN : A partir del 20 de diciembre de 2018, este error está marcado como CORREGIDO. El parche aparecerá en gcc 9 con la adición de una nueva -Waddress-of-packed-member
opción, habilitada de forma predeterminada.
Cuando se toma la dirección del miembro empaquetado de una estructura o unión, puede resultar en un valor de puntero no alineado. Este parche agrega -Waddress-of-packed-member para verificar la alineación en la asignación del puntero y advertir sobre direcciones no alineadas y punteros no alineados.
Acabo de crear esa versión de gcc desde la fuente. Para el programa anterior, produce estos diagnósticos:
c.c: In function ‘main’:
c.c:10:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member]
10 | int *p0 = &arr[0].x;
| ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member]
11 | int *p1 = &arr[1].x;
| ^~~~~~~~~
Como dijo ams anteriormente, no tome 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 es así, no se puede culpar 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 forma 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 " con muerte cerebral que no merece ser abordada: desdén comprensible, pero no ayuda cuando un programador sin experiencia se topa con él.
Considera lo siguiente:
struct __attribute__((__packed__)) my_struct {
char c;
int i;
};
struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;
Aquí, el tipo de a
es una estructura empaquetada (como se definió anteriormente). De manera similar, b
es un puntero a una estructura empaquetada. El tipo de expresión a.i
es (básicamente) un valor int l con alineación de 1 byte. c
y d
ambos son normales int
s. Al leer a.i
, el compilador genera código para acceso no alineado. Cuando lees b->i
, b
el tipo todavía sabe que está lleno, así que tampoco hay problema. e
es un puntero a un int alineado de un byte, por lo que el compilador también sabe cómo desreferenciarlo correctamente. Pero cuando realiza la asignación f = &a.i
, está almacenando el valor de un puntero int no alineado en una variable de puntero int alineado; ahí es donde salió mal. Y estoy de acuerdo, gcc debería tener esta advertencia habilitada de forma predeterminada (ni siquiera en -Wall
o -Wextra
).
Es perfectamente seguro siempre que siempre acceda a los valores a través de la estructura mediante la notación .
(punto) o ->
.
Lo que no es seguro es tomar el puntero de datos no alineados y luego acceder a ellos sin tenerlo en cuenta.
Además, aunque se sabe que cada elemento de la estructura está desalineado, se sabe que está desalineado de una manera particular , por lo que la estructura en su conjunto debe estar alineada como espera el compilador o habrá problemas (en algunas plataformas, o en el futuro si se inventa una nueva forma de optimizar los accesos no alineados).
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 de 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 la norma C11 establece:
6 Se ofrece una garantía especial para simplificar el uso de uniones: si una unión contiene varias estructuras que comparten una secuencia inicial común (ver más abajo), y si el objeto de unión actualmente contiene una de estas estructuras, se permite inspeccionar la parte inicial común de cualquiera de ellos en cualquier lugar donde sea visible una declaración del tipo de unión completada. Dos estructuras comparten una secuencia inicial común si los miembros correspondientes tienen tipos compatibles (y, para campos de bits, los mismos anchos) para una secuencia de uno o más miembros iniciales.
...
9 EJEMPLO 3 El siguiente es un fragmento válido:
union { struct { int alltypes; }n; struct { int type; int intnode; } ni; struct { int type; double doublenode; } nf; }u; u.nf.type = 1; u.nf.doublenode = 3.14; /* ... */ if (u.n.alltypes == 1) if (sin(u.nf.doublenode) == 0.0) /* ... */
Cuando __attribute__((packed))
se introduce, se rompe esto. El siguiente ejemplo se ejecutó en Ubuntu 16.04 x64 usando gcc 5.4.0 con las optimizaciones deshabilitadas:
#include <stdio.h>
#include <stdlib.h>
struct s1
{
short a;
int b;
} __attribute__((packed));
struct s2
{
short a;
int b;
};
union su {
struct s1 x;
struct s2 y;
};
int main()
{
union su s;
s.x.a = 0x1234;
s.x.b = 0x56789abc;
printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
return 0;
}
Producción:
sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678
Aunque struct s1
y struct s2
tienen una "secuencia inicial común", el empaquetado aplicado al primero significa que los miembros correspondientes no viven en el mismo desplazamiento de bytes. El resultado es que el valor escrito en member x.b
no es el mismo que el valor leído en member y.b
, aunque el estándar dice que deberían ser iguales.