¿El __attribute __ ((empaquetado)) / #pragma pack de gcc no es seguro?

164

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 intobjetos tengan una alineación de 4 bytes, __attribute__((packed))puede hacer que los intmiembros 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?

Keith Thompson
fuente
1
El informe de error de gcc ahora está marcado como FIJO con la adición de una advertencia en la asignación del puntero (y una opción para deshabilitar la advertencia). Detalles en mi respuesta .
Keith Thompson

Respuestas:

148

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 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 intobjeto 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:

#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 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 xmiembro esté alineado correctamente. Con una matriz de dos struct fooobjetos, al menos uno u otro tendrá un xmiembro desalineado ).

(En este caso, p0apunta a una dirección desalineada, porque apunta a un intmiembro empaquetado que sigue a un charmiembro. p1Está alineado correctamente, ya que apunta al mismo miembro en el segundo elemento de la matriz, por lo que hay dos charobjetos que lo preceden - y en SPARC Solaris, la matriz arrparece estar asignada a una dirección que es par, pero no un múltiplo de 4.)

Al referirse al miembro xde a struct foopor nombre, el compilador sabe que xestá potencialmente desalineado y generará código adicional para acceder a él correctamente.

Una vez que la dirección de arr[0].xo arr[1].xse ha almacenado en un objeto puntero, ni el compilador ni el programa en ejecución saben que apunta a un intobjeto 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-memberopción, habilitada por defecto.

Cuando se toma la dirección del miembro empaquetado de struct o union, puede resultar en un valor de puntero no alineado. Este parche agrega -Waddress-of-pack-member para verificar la alineación en la asignación del puntero y advertir la dirección no alineada, así como el puntero no alineado

Acabo de construir 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;
      |               ^~~~~~~~~
Keith Thompson
fuente
1
está potencialmente desalineado y generará ... ¿qué?
Almo
55
los elementos de estructura desalineados en ARM hacen cosas extrañas: algunos accesos causan fallas, otros causan que los datos recuperados se reorganicen de manera contra intuitiva o incorporen datos inesperados adyacentes.
wallyk
8
Parece que el embalaje en sí es seguro, pero la forma en que se usan los miembros embalados puede ser inseguro. Las CPU más antiguas basadas en ARM tampoco admitían accesos de memoria no alineados, las versiones más nuevas sí, pero sé que Symbian OS todavía no permite accesos no alineados cuando se ejecuta en estas versiones más nuevas (el soporte está desactivado).
James
14
Otra forma de solucionarlo dentro de gcc sería utilizar el sistema de tipos: requiere que los punteros a miembros de estructuras empaquetadas solo se puedan asignar a punteros que estén marcados como empaquetados (es decir, potencialmente no alineados). Pero en realidad: estructuras llenas, solo di que no.
caf
9
@Flavius: Mi objetivo principal era obtener la información. Ver también meta.stackexchange.com/questions/17463/…
Keith Thompson
62

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-alignopción, no está habilitada de manera predeterminada ni con -Wallo -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:

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 aes una estructura empaquetada (como se definió anteriormente). Del mismo modo, bes un puntero a una estructura empaquetada. El tipo de expresión a.ies (básicamente) un valor int l con alineación de 1 byte. cy dambos son normales ints. Al leer a.i, el compilador genera código para acceso no alineado. Cuando lees b->i, bel tipo todavía sabe que está empaquetado, así que no hay problema tampoco. ees 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ón f = &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 -Wallo -Wextra).

Daniel Santos
fuente
66
¡+1 para explicar cómo usar punteros con estructuras no alineadas!
Soumya
@Soumya ¡Gracias por los puntos! :) Tenga en cuenta, sin embargo, que __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.
Daniel Santos
44
@Soumya Lamentablemente, __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.
Daniel Santos
@DanielSantos: Un compilador de calidad que uso (Keil) reconoce los calificadores "empaquetados" para los punteros; si una estructura se declara "empaquetada", tomar la dirección de un uint32_tmiembro generará un uint32_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]
supercat
49

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).

ams
fuente
Hmm, me pregunto qué pasa si pones una estructura empaquetada dentro de otra estructura empaquetada donde la alineación sería diferente. Pregunta interesante, pero no debería cambiar la respuesta.
enms.
GCC tampoco siempre alineará la estructura misma. Por ejemplo: struct foo {int x; char c; } __attribute __ ((empaquetado)); barra de estructura {char c; struct foo f; }; Descubrí que bar :: f :: x no estará necesariamente alineado, al menos en ciertos sabores de MIPS.
Anton
3
@antonm: Sí, una estructura dentro de una estructura empaquetada puede estar desalineada, pero, de nuevo, el compilador sabe cuál es la alineación de cada campo, y es perfectamente seguro siempre que no intente utilizar punteros en la estructura. Debería imaginar una estructura dentro de una estructura como una serie plana de campos, con el nombre adicional solo para facilitar la lectura.
enms.
6

Usar este atributo definitivamente no es seguro.

Una cosa particular que rompe es la capacidad de un unionque 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 :

6 Se hace una garantía especial para simplificar el uso de las uniones: si una unión contiene varias estructuras que comparten una secuencia inicial común (ver abajo), y si el objeto de la unión actualmente contiene una de estas estructuras, se permite inspeccionar el Parte inicial común de cualquiera de ellos en cualquier lugar que sea visible una declaración del tipo completo de la unión. 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 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;
}

Salida:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

A pesar de que struct s1y struct s2tener 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 member x.bno es el mismo que el valor leído de member y.b, aunque el estándar dice que deberían ser iguales.

dbush
fuente
Uno podría argumentar que si empaca una de las estructuras y no la otra, entonces no va a esperar que tengan diseños consistentes. Pero sí, este es otro requisito estándar que puede violar.
Keith Thompson
1

(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:

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

Entonces puedo declarar algo como

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

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,

John Allsup
fuente
A menos que me falte algo, esto no responde la pregunta. Usted argumenta que el empaque de la estructura es conveniente (lo cual es), pero no aborda la cuestión de si es seguro. Además, usted afirma que las penalizaciones de rendimiento para lecturas no alineadas; eso es cierto para x86, pero no para todos los sistemas, como lo demostré en mi respuesta.
Keith Thompson