c cross-platform portability low-level bit-fields

c - ¿Por qué bit endianness es un problema en bitfields?



cross-platform portability (6)

Por lo que yo entiendo, bitfields son construcciones puramente compiladoras

Y eso es parte del problema. Si el uso de los campos de bits estuviera restringido a lo que el compilador "poseía", entonces la forma en que el compilador empaquetaba los bits o los ordenaba no le importaría a nadie.

Sin embargo, los campos de bits probablemente se utilizan mucho más a menudo para modelar construcciones que son externas al dominio del compilador: registros de hardware, el protocolo ''cable'' para comunicaciones o el formato de formato de archivo. Estas cosas tienen requisitos estrictos de cómo se deben diseñar los bits, y usar campos de bits para modelarlos significa que tienes que confiar en la definición de la implementación y, lo que es peor, en el comportamiento no especificado de cómo el compilador distribuirá el campo de bits. .

En resumen, los campos de bits no se especifican lo suficientemente bien como para hacerlos útiles para las situaciones en las que parecen ser más comúnmente utilizados.

Cualquier código portátil que use bitfields parece distinguir entre plataformas de poco y gran extremo. Ver la declaración de struct iphdr en el kernel de Linux para un ejemplo de dicho código. No entiendo por qué bit endianness es un problema en absoluto.

Por lo que yo entiendo, bitfields son construcciones puramente compiladoras, usadas para facilitar las manipulaciones a nivel de bit.

Por ejemplo, considere el siguiente bitfield:

struct ParsedInt { unsigned int f1:1; unsigned int f2:3; unsigned int f3:4; }; uint8_t i; struct ParsedInt *d = &i; Aquí, escribir d->f2 es simplemente una forma compacta y legible de decir (i>>1) & (1<<4 - 1) .

Sin embargo, las operaciones de bits están bien definidas y funcionan independientemente de la arquitectura. Entonces, ¿cómo es que los bitfields no son portátiles?


Los accesos de campo de bits se implementan en términos de operaciones en el tipo subyacente. En el ejemplo, unsigned int . Entonces, si tienes algo como:

struct x { unsigned int a : 4; unsigned int b : 8; unsigned int c : 4; };

Cuando accede al campo b , el compilador accede a un unsigned int entero unsigned int y luego cambia y enmascara el rango de bits apropiado. (Bueno, no tiene por qué ser así , pero podemos pretender que sí).

En big endian, el diseño será algo como esto (el bit más significativo primero):

AAAABBBB BBBBCCCC

En little endian, el diseño será así:

BBBBAAAA CCCCBBBB

Si desea acceder al diseño de big endian desde little endian o viceversa, tendrá que hacer un trabajo extra. Este aumento en la portabilidad tiene una penalización en el rendimiento, y como el diseño de la estructura ya no es portátil, los implementadores del lenguaje optaron por la versión más rápida.

Esto hace muchas suposiciones. También tenga en cuenta que sizeof(struct x) == 4 en la mayoría de las plataformas.


Los campos de bits se almacenarán en un orden diferente dependiendo de la endianidad de la máquina, esto puede no importar en algunos casos, pero en otros puede ser importante. Digamos, por ejemplo, que su estructura ParsedInt representa banderas en un paquete enviado a través de una red, una pequeña máquina endian y una máquina big endian leen esas banderas en un orden diferente del byte transmitido, lo que obviamente es un problema.


Para hacer eco de los puntos más sobresalientes: si está utilizando esto en una única plataforma compilador / hardware como una construcción de software único, entonces endianidad no será un problema. Si está usando código o datos en múltiples plataformas O necesita hacer coincidir diseños de bits de hardware, entonces ES un problema. Y una gran cantidad de software profesional es multiplataforma, por lo tanto, tiene que importarle.

Este es el ejemplo más simple: tengo un código que almacena números en formato binario en el disco. Si no escribo y leo estos datos en el disco explícitamente byte por byte, entonces no será el mismo valor si se lee de un sistema endian opuesto.

Ejemplo concreto:

int16_t s = 4096; // un número de 16 bits firmado ...

Digamos que mi programa se envía con algunos datos en el disco en el que quiero leer. Digamos que quiero cargarlo como 4096 en este caso ...

fread ((void *) & s, 2, fp); // leerlo desde el disco como binario ...

Aquí lo leo como un valor de 16 bits, no como bytes explícitos. Eso significa que si mi sistema coincide con la endianidad almacenada en el disco, obtengo 4096, y si no lo hace, obtengo 16 !!!!!

Por lo tanto, el uso más común de endianness es cargar números binarios a granel, y luego hacer un bswap si no coincide. En el pasado, almacenamos datos en el disco como Big Endian porque Intel era el hombre extraño y proporcionó instrucciones de alta velocidad para intercambiar los bytes. Hoy en día, Intel es tan común que a menudo hace que Little Endian sea el predeterminado y se intercambia cuando está en un sistema endian grande.

Un enfoque neutral más lento, pero endian, es hacer TODAS las E / S por bytes, es decir:

uint_8 ubyte; int_8 sbyte; int16_t s; // lee s en endian neutral way

// Elijamos little endian como nuestro orden de bytes elegido:

fread ((void *) y ubyte, 1, fp); // Solo lee 1 byte a la vez fread ((void *) & sbyte, 1, fp); // Solo lee 1 byte a la vez

// Reconstruir

s = ubyte | (sByte << 8);

Tenga en cuenta que esto es idéntico al código que escribiría para hacer un intercambio de endian, pero ya no necesita verificar el endianness. Y puede usar macros para que esto sea menos doloroso.

Usé el ejemplo de datos almacenados utilizados por un programa. La otra aplicación principal mencionada es escribir registros de hardware, donde esos registros tienen un orden absoluto. Un lugar MUY COMUN que aparece es con gráficos. ¡Obtenga el endianness incorrecto y sus canales de color rojo y azul se invierten! Una vez más, se trata de una cuestión de portabilidad: simplemente puede adaptarse a una plataforma de hardware y una tarjeta gráfica, pero si desea que su mismo código funcione en diferentes máquinas, debe probarlo.

Aquí hay una prueba clásica:

typedef union {uint_16 s; uint_8 b [2]; } EndianTest_t;

Prueba EndianTest_t = 4096;

if (test.b [0] == 12) printf ("¡Big Endian Detectado! / n");

Tenga en cuenta que también existen problemas de bitfield, pero son ortogonales a problemas de endianness.


Según el estándar C, el compilador puede almacenar el campo de bits de forma aleatoria. Nunca se puede hacer ninguna suposición acerca de dónde se asignan los bits. Aquí hay algunas cosas relacionadas con el campo de bits que no están especificadas por el estándar C:

Comportamiento no especificado

  • La alineación de la unidad de almacenamiento direccionable asignada para contener un campo de bits (6.7.2.1).

Comportamiento definido por la implementación

  • Si un campo de bits puede ubicarse a horcajadas en un límite de una unidad de almacenamiento (6.7.2.1).
  • El orden de asignación de los campos de bits dentro de una unidad (6.7.2.1).

Big / little endian también está definido por implementación. Esto significa que su estructura se puede asignar de las siguientes maneras (suponiendo 16 bit ints):

PADDING : 8 f1 : 1 f2 : 3 f3 : 4 or PADDING : 8 f3 : 4 f2 : 3 f1 : 1 or f1 : 1 f2 : 3 f3 : 4 PADDING : 8 or f3 : 4 f2 : 3 f1 : 1 PADDING : 8

¿Cuál aplica? Adivina o lee en profundidad la documentación de tu compilador. Agregue la complejidad de los enteros de 32 bits, en grande o pequeño endian, a esto. A continuación, agregue el hecho de que el compilador puede agregar cualquier número de bytes de relleno en cualquier lugar dentro de su campo de bit, porque se trata como una estructura (no puede agregar relleno al principio de la estructura, sino en cualquier otro lugar).

Y entonces ni siquiera he mencionado lo que sucede si usas plain "int" como bit-field type = comportamiento definido por la implementación, o si usas cualquier otro tipo que no sea (unsigned) int = implementation-defined behavior.

Entonces, para responder a la pregunta, no existe el código portátil de campo de bits, porque el estándar C es extremadamente impreciso con respecto a cómo deben implementarse los campos de bits. Lo único que se puede confiar en los campos de bits es que son fragmentos de valores booleanos, donde el programador no se preocupa por la ubicación de los bits en la memoria.

La única solución portátil es utilizar los operadores de bits en lugar de los campos de bits. El código máquina generado será exactamente el mismo, pero determinista. Los operadores de bits son 100% portátiles en cualquier compilador de C para cualquier sistema.


ISO / CEI 9899: 6.7.2.1 / 10

Una implementación puede asignar cualquier unidad de almacenamiento direccionable lo suficientemente grande como para contener un bit-campo. Si queda suficiente espacio, un campo de bits que sigue inmediatamente a otro campo de bits en una estructura debe ser empacado en bits adyacentes de la misma unidad. Si queda un espacio insuficiente, si un campo de bits que no encaja se coloca en la siguiente unidad o se superpone a unidades adyacentes, se define la implementación. El orden de asignación de campos de bits dentro de una unidad (de orden alto a orden bajo o de orden bajo a orden superior) se define por implementación. La alineación de la unidad de almacenamiento direccionable no está especificada.

Es más seguro usar operaciones de cambio de bits en lugar de hacer suposiciones sobre el orden o la alineación del campo de bits cuando se intenta escribir código portátil, independientemente de la endianidad o bitness del sistema.

También vea EXP11-C. No aplique operadores que esperan un tipo de datos de un tipo incompatible .