online c gcc

online - gcc flags list



¿El paquete__attribute__((packed))/#pragma de gcc es inseguro? (4)

(El siguiente es un ejemplo muy artificial preparado para ilustrar). Un uso principal de las estructuras empaquetadas es cuando se tiene una secuencia de datos (digamos 256 bytes) a los cuales se les quiere dar un significado. Si tomo un ejemplo más pequeño, supongamos que tengo un programa ejecutándose en mi Arduino que envía vía serie un paquete de 16 bytes que tienen 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 así 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 de destino de destino a través de aStruct.targetAddr en lugar de manipular la aritmética del puntero.

Ahora con las cosas de alineación sucediendo, tomar un puntero void * en memoria a los datos recibidos y enviarlo a myStruct * no funcionará a menos que el compilador trate la estructura como empaquetada (es decir, almacena datos en el orden especificado y usa exactamente 16 bytes para este ejemplo). Existen penalizaciones de rendimiento para las lecturas no alineadas, por lo que el uso de estructuras compuestas para datos con los que su programa está trabajando activamente no es necesariamente una buena idea. Pero cuando su programa se suministra con una lista de bytes, las estructuras empaquetadas hacen que sea más fácil escribir 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 la aritmética del puntero detrás de las escenas. En resumen, las estructuras empaquetadas son para manejar eficientemente los datos empaquetados, y los datos empaquetados pueden ser los que se le den a su programa para trabajar. En su mayor parte, el código debe leer valores de la estructura, trabajar con ellos y volver a escribirlos cuando haya terminado. Todo lo demás debe hacerse fuera de la estructura empaquetada. Parte del problema es el bajo nivel de cosas que C intenta esconder del programador y el salto en el aro que se necesita si tales cosas realmente le importan al programador. (Casi necesita una construcción de ''diseño de datos'' diferente en el lenguaje para poder decir ''esto tiene 48 bytes de longitud, foo se refiere a los datos de 13 bytes y debe interpretarse así'', y una construcción de datos estructurados separada, donde dices ''Quiero una estructura que contenga dos ints, llamada alice y bob, y un flotador llamado carol, y no me importa cómo lo implementes'' - en C ambos casos de uso se calzan en la construcción de la estructura).

En C, el compilador establecerá 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 el relleno, lo que permite que los miembros de la estructura se desalineen. Por ejemplo, si el sistema normalmente requiere que todos los objetos int tengan una alineación de 4 bytes, __attribute__((packed)) puede hacer que los miembros int struct se asignen en offsets impares.

Citando la documentación de gcc:

El atributo `packed ''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` aligned''.

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 algún caso en que esto no sea seguro? ¿El compilador siempre genera código correcto (aunque más lento) para acceder a los miembros desalineados de las estructuras empaquetadas? ¿Es posible que lo haga en todos los casos?


Como dije anteriormente, no tome un puntero a un miembro de una estructura empaquetada. Esto es simplemente jugando con fuego. Cuando dices __attribute__((__packed__)) o #pragma pack(1) , lo que realmente estás diciendo es "Hey gcc, realmente sé lo que estoy haciendo". Cuando resulta que no es así, no se puede culpar correctamente al compilador.

Sin embargo, quizás podamos culpar al compilador por su complacencia. Mientras que gcc tiene una opción -Wcast-align , no está habilitada por defecto ni con -Wall o -Wextra . Esto aparentemente se debe a que los desarrolladores de gcc que consideran que este tipo de código es una " abomination " con muerte cerebral indigna de abordar, es comprensible desdeñable, pero no ayuda cuando un programador inexperto se topa con ello.

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 define arriba). Del mismo modo, b es un puntero a una estructura empaquetada. El tipo de expresión ai es (básicamente) un l-value int l-value con alineación de 1 byte. d son ambos int normales. Al leer ai , el compilador genera código para el acceso no alineado. Cuando lee b->i , el tipo de b todavía sabe que está empaquetado, por lo que no hay problema tampoco. e es un puntero a un int alineado con 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 alineada - ahí es donde salió mal. Y estoy de acuerdo, gcc debería tener esta advertencia habilitada por defecto (ni siquiera en -Wall o -Wextra ).


Es perfectamente seguro siempre que siempre acceda a los valores a través de la estructura a través de . (punto) o -> notación.

Lo que no es seguro es tomar el puntero de los datos no alineados y luego acceder a ellos sin tener eso en cuenta.

Además, aunque se sabe que cada elemento de la estructura no está alineado, se sabe que está desalineado de una manera particular , por lo que la estructura como un todo debe alinearse como espera el compilador o habrá problemas (en algunas plataformas, o en el futuro si se inventa una nueva forma para optimizar los accesos no alineados).


Sí, __attribute__((packed)) es potencialmente inseguro en algunos sistemas. El síntoma probablemente no aparezca 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 desreferencia un puntero int* 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 objeto int desalineado causa un error de bus, bloqueando el programa.

También ha habido sistemas en los que un acceso desalineado ignora silenciosamente los bits de bajo orden de la dirección, lo que hace que acceda a la porción incorrecta de la memoria.

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 x86 Ubuntu 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 se gcc packed.c -o packed .

(Un programa que usa una única estructura en lugar de una matriz no muestra el problema de manera confiable, ya que el compilador puede asignar la estructura a una dirección impar para que el miembro x esté correctamente alineado. Con una matriz de dos objetos struct foo , al menos uno o el otro tendrá un x miembro desalineado).

(En este caso, p0 apunta a una dirección desalineada, porque apunta a un miembro int empaquetado que sigue a un miembro char . p1 está correctamente alineado, ya que apunta al mismo miembro en el segundo elemento de la matriz, por lo que hay dos objetos char que lo preceden, y en SPARC Solaris, la matriz arr parece estar asignada a una dirección que es par, pero no a un múltiplo de 4.)

Cuando se hace referencia al miembro x de una struct foo por nombre, el compilador sabe que x está potencialmente desalineada 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 de puntero, ni el compilador ni el programa en ejecución saben que apunta a un objeto int desalineado. Simplemente asume que está alineado correctamente, lo que resulta (en algunos sistemas) en un error de bus u otro fallo similar.

Reparar esto en gcc sería, creo, 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) demostrando en tiempo de compilación que el puntero no apunta a un miembro desalineado de una estructura empaquetada, o (b) generando un 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 solucionarlo, pero la documentación debería mencionarlo (actualmente no).