Monthly Archives: Marzo 2010

GCC y el padding

0
Filed under Embedded, GCC, General, Programación
Tagged as , , ,

Introducción

Hace poco, en el trabajo, me encontraba realizando la implementación del protocolo descrito en la RFC 908 para usarla en un pequeño microcontrolador de Atmel.

Una vez implementado y compilado todo, me dispuse a realizar las correspondientes pruebas y vi que en ciertas funciones sucedían cosas muy extrañas. En concreto, tenía definidas varias estructuras en las que se almacenaba toda la información de estado y control del protocolo y descubrí que al intentar recorrer todos sus miembros con un puntero a char las cosas no cuadraban. Leía valores que se suponía que no estaban ahí o al calcular el tamaño de los mensajes basándome en el tamaño de las estructuras (calculadas usando sizeof) estos no eran los que tenían que ser….

¿Qué estaba sucediendo?

El problema: Padding

Pues si. Enseguida imaginé cual podría ser el problema: el Padding. La verdad es que es un error un poco de novatos, pero yo caí. Generalmente los programadores en lenguajes como C raramente suelen tener en cuenta el tema del padding a diseñar sus programas, puesto que suponen que el compilador se encargará de optimizar nuestro código y además, rara vez se trabaja a un nivel tan bajo como para que ésto sea un problema.

La cosa cambia si estás diseñando código para correr en un dispositivo empotrado con poca memoria o tienes que hacer manipulaciones a muy bajo nivel sobre las estructuras. En mi caso, se daban las dos situaciones, así que me puse a investigar un poco y ésto fue lo que aprendí.

¿Qué es exactamente el padding? Bueno, empecemos por el principio:

El Alineamiento

¿Y ésto qué es?. Bueno, dependiendo del tipo de máquina que estemos utilizando y del tipo de dato con el que estemos trabajando, el rango de direcciones en las que se puede almacenar dicho dato debe cumplir una u otra característica. Me explico: supongamos que estamos trabajando en una máquina de 32 bits. En éste caso, tendremos una máquina con una alineación de memoria de 4 bytes. Es decir, cualquier lectura de memoria se hace de cuatro en cuatro bytes y todas las posibles direcciones de memoria que se leen serán múltiplo de 4. Por tanto, una lectura, supongamos en la dirección 0x00abcd0034 sería correcta, mientras que si intentásemos leer de la dirección 0x00abcd0031 no podríamos, pues esa dirección no está alineada a 4 bytes (no es múltiplo de 4). Si queremos leer ese byte en concreto, deberemos leer la dirección 0x00abcd0030 y después acceder a los bits <8:15>
Hay que hacer una pequeña aclaración y comentar que ésto es para el caso de que nuestra máquina sea de 32 bits pero use una memoria de 8 bits. También podemos tener máquinas con un bus de memoria de igual longitud que los datos que utiliza, en cuyo caso, el alineamiento de las posiciones de memoria sería 1, es decir, se puede leer y escribir en cualquier dirección, pero se leerán 32 bits (o los que tenga el ancho del bus) de cada vez. Para lo que voy a explicar, me parece mas conveniente el modelo de máquina de 32 bits con un bus de 8, aunque esto es aplicable a cualquier tipo de arquitectura.

Si en la máquina de la que estamos hablando queremos guardar un char, dicha variable la podremos guardar, como podemos imaginar, en cualquier dirección (un char, como todos sabemos, ocupa 8 bits). Decimos que el alineamiento de éste tipo de variables es de un byte. Ésto sucede porque, en nuestra máquina de 32 bits, para leer un char, leeremos 32 bits (4 bytes) de una vez, y entre esos 4 bytes, estará el que buscamos.
La cosa cambia si ahora en lugar de querer almacenar un simple char queremos guardar un short (típicamente 16 bits). En éste caso solo podremos almacenarlo en direcciones múltiplo de 2. ¿Y ésto a cuento de qué?. Pues como algunos habréis imaginado (y si no lo habéis imaginado yo os lo cuento), si en cada lectura que realiza nuestro procesador leemos 4 bytes y un short son 2 bytes, tendremos que colocar dicha variable bien en los dos primeros bytes o bien en los dos últimos. Por supuesto también podríamos colocarlo en los bytes 2 y 3, pero si permitimos ese tipo de cosas nada impediría que el primer byte de un short cayese en el último byte de una palabra leída y el segundo en el primer byte de la palabra siguiente. Mas claramente: nuestra máquina lee las direcciones 0×0000, 0×0004, 0×0008, etc…. Nuestro short correría el riesgo de caer entre las direcciones 0×0003 y 0×0004. ¿Cual es la pega de ésto? Obviamente que el procesador debería realizar dos lecturas a memoria: una a la dirección 0×0000 y otra a la dirección 0×0004 para recuperar los dos “trozos” de short. Ahora se ve claro por qué hace falta definir ésto del alineamiento de los tipos de datos: Ahorra trabajo y tiempo al procesador.
Decimos entonces que un short está alineado a dos bytes.

Como podremos imaginar, para el caso de un int o incluso de un double, la alineación tiene que ser forzosamente a 4 bytes, es decir, deben de estar almacenados en direcciones múltiplos de 4.

Ahora si, el Padding

Muy bien, ahora que ya hemos entendido en qué consiste la alineación, podemos hablar del Padding, que no es ni mas ni menos que bytes de “relleno” que se utilizan para forzar el alineamiento de los miembros de una estructura, en el caso del lenguaje C. Veamoslo con un ejemplo:
En el siguiente código podemos ver que tenemos definida una estructura formada por un char y un int:

  1. #include <stdio.h>
  2.  
  3. int main (int argc, char** argv){
  4.  
  5.   struct {
  6.     char a;
  7.     int b;
  8.   } padding;
  9.  
  10.   char* p = (char *)&padding;
  11.  
  12.   padding.a = ‘z’;
  13.   padding.b = 0x6fabada6;
  14.  
  15.   printf("longitud de padding: %i\n", sizeof(padding));
  16.   printf("Datos originales:\n\ta —> %c\n\tb —> 0x%x\n", padding.a, padding.b);
  17.  
  18.   *p = ‘a’; p++;
  19.   *p = 0xbe; p++;
  20.   *p = 0xba; p++;
  21.   *p = 0×12; p++;
  22.   *p = 0×34;
  23.  
  24.   printf("Datos modificados:\n\ta —> %c\n\tb —> 0x%x\n", padding.a, padding.b);
  25. }

Antes de compilar ni ejecutar nada podemos suponer que el tamaño de nuestra estructura (en una máquina de 32 bits) debería ser de 5 bytes: 4 bytes del int y 1 del char. El código simplemente imprime la longitud de la estructura, los datos originalmente almacenados en ella y después, a través de un puntero char recorre dicha estructura y modifica los datos. Finalmente, vuelve a imprimir los datos guardados en la estructura.
Ahora ya podemos compilarlo y ejecutarlo obteniendo la siguiente salida:

  1. longitud de padding: 8
  2. Datos originales:
  3.         a —> z
  4.         b —> 0x6fabada6
  5. Datos modificados:
  6.         a —> a
  7.         b —> 0x6fabad34

:-| ¿qué ha pasado? ¿y esa longitud 8 en lugar de 5? Sencillo: nuestra máquina es de 32 bits. Para poder forzar el alineamiento correcto de las variables que componen la estructura tiene “rellenar” el hueco ocupado entre el char y el int para que éste último comience en una dirección múltiplo de 4 (o en el caso de un PC de 32 bits, se lea en la siguiente dirección de memoria, porque en éste caso, el bus de la memoria coincide con el del micro). Es decir, es como si hubiésemos declaro padding de ésta otra forma:

  1. struct {
  2.   char a;
  3.   char dummy1;
  4.   char dummy2;
  5.   char dummy3;
  6.   int b;
  7. } padding;

de modo que dummy? son solo para rellenar (padding) y hacer que b empiece donde debe, respetando así el alineamiento. Si no fuese así y quisiéramos acceder a a, seguiríamos usando una lectura, mientras que para acceder a b la cosa cambiaría, ya que necesitaríamos una lectura para acceder a los tres primeros bytes y una segunda lectura mas para acceder al último. Después habría que realizar desplazamientos y ajustes para obtener el valor final.
De ahí que veamos otro efecto colateral a la salida de la ejecución del código: El último valor leído de b no coincide con el esperado. De hecho solo hemos modificado el primer byte. Los otros tres bytes que hemos recorrido han sido los del padding.

Ésto abre la veda para hacer pequeños “experimentos”. Por ejemplo, si cambiamos a y b de posiciones relativas veremos que el tamaño sigue siendo 8. ¿De verdad sigue haciendo falta relleno? Ahora se supone que leemos primero el int y que por tanto b lo podremos leer sin problemas…. Pues la respuesta es si y no. En realidad ahora no hay padding, pero si que es cierto que la estructura sigue ocupando 8 bytes en memoria, pues tendremos que hacer dos lecturas para acceder a todos sus miembros.

Pero podemos hacer mas cosillas: Por ejemplo, podemos declarar otro miembro de tipo short entre a y b y ver qué pasa con el tamaño. ¿Ha cambiado? ¡¡NO!! ¿y eso?. Pues porque al encontrarse el short entre un char y un int, puede utilizar tres de los bytes de padding para almacenarse, por lo que ahora habrá un padding de un solo byte. Si en lugar de eso declaramos el short después de b, veremos que la cosa cambia bastante. Ahora estaremos usando 12 bytes. Queda bastante claro todo ésto ¿no?.

Evitando el padding con GCC

Una pregunta bastante obvia que puede asaltar a cualquier lector es: ¿y si no quiero que haya padding porque me hace desperdiciar memoria o me viene fatal para recorrer mi estructura?. Bueno, GCC permite forzar el padding que nosotros queramos usando pragmas. En concreto se usa el pragma pack. Veámoslo con un ejemplo sobre nuestro código anterior:

  1. #include <stdio.h>
  2.  
  3. int main (int argc, char** argv){
  4.  
  5. #pragma pack(push,1)
  6.   struct {
  7.     char a;
  8.     int b;
  9.   } padding;
  10. #pragma pack(pop)
  11.  
  12.   char* p = (char *)&padding;
  13.  
  14.   padding.a = ‘z’;
  15.   padding.b = 0x6fabada6;
  16.  
  17.   printf("longitud de padding: %i\n", sizeof(padding));
  18.   printf("Datos originales:\n\ta —> %c\n\tb —> 0x%x\n", padding.a, padding.b);
  19.  
  20.   *p = ‘a’; p++;
  21.   *p = 0xbe; p++;
  22.   *p = 0xba; p++;
  23.   *p = 0×12; p++;
  24.   *p = 0×34;
  25.  
  26.   printf("Datos modificados:\n\ta —> %c\n\tb —> 0x%x\n", padding.a, padding.b);
  27. }

El primer pragma establece que se fuerce un padding de 1 byte y lo “apila” en una pila que usa el compilador. De ésta forma podemos ir forzando varios paddings y cuando se use pragma pack(pop) sacaremos el último padding que hayamos usado y volveremos al anterior. En nuestro caso, volveremos al padding por defecto de nuestra máquia, puesto que solo hemos hecho un push.

Si ahora ejecutamos el código veremos que si que sucede lo esperado: padding ocupa 5 bytes y recorriendo la estructura con un puntero a char conseguimos modificar todos los datos como esperábamos.

Yo personalmente opto por no “forzar” las cosas. Un consejo para definir estructuras es ir declarando sus componentes por orden de menor a mayor tamaño. De ésta forma, quedarán organizados de una forma bastante óptima. También habrá que cuidar los recorridos por los miembros de la estructura, utilizando funciones específicas para ello.

Claro que también puede haber situaciones en las que no quede mas remedio. Por ejemplo, otra cosa en la que actualmente ando liado es la implementanción de un pequeño driver para un sistema de archivos FAT32 que deberá correr en microcontroladores AVR con muy pocas capacidades. Para poder interpretar las tablas de asignación de archivos he de leer bloques del medio en el que tengo almacenados los datos y después interpretar esa información acorde a la estructura que define a dicha tabla. El problema me viene, al igual que comenté al comienzo de ésta entrada, en que si bien en el caso del microcontrolador (con un bus de 8 bits) los datos leídos casan perfectamente con las estructuras necesarias, para el caso de los tests en el PC esto no es así, ya que las estructuras utilizan muchos tipos de 8 y 16 bits y el padding generado para forzar el alineamiento hace que los datos leídos no concuerden con los campos a los que tienen que ir a parar. En éste caso, tengo dos opciones: o una vez leído el bloque (y guardado temporalmente en un array de char) voy rellenando todas las estructuras campo a campo (algo muy muy lento para un microcontrolador y que necesita gran cantidad de código) o bien fuerzo el alineamiento a un byte para evitarme el padding y genero un puntero a la estructura que me interesa guardando en él la dirección de comienzo de mi array a char. De ésta manera podré recorrer mas fácilmente la estructura, sabiendo que para el micro no supondrá coste alguno y que para el PC generaré algo de código extra (al tener que forzar el alineamiento), pero realmente como mi intención es usar el PC sólo para validar los algorítmos, no importa que sea algo ineficiente si con ello consigo el efecto deseado.

Bajo mi punto de vista (y por lo que acabamos de ver también), creo que forzar paddings puede suponer que en algunas arquitecturas el procesador tenga que realizar mas trabajo. En un PC puede que sea (o no) despreciable, pero desde luego en un sistema empotrado ello supone pérdida de tiempo, consumo de energía y consumo de memoria de código (necesitaremos mas código para codificar las operaciones, como es lógico suponer). Ya es decisión de cada uno optar por una u otra forma de implementar.

Referencias

Pues como siempre, navegando por Internet uno encuentra muchas cosas. En http://bytes.com/topic/c/answers/543879-what-structure-padding encontré una explicación bastante clara de todo ésto del alineamiento y del padding.