Uso de nibbles (variables de 4 bits) en Windows C/C++

5 minutos de lectura

Estoy programando encabezados de red y muchos protocolos usan campos de 4 bits. ¿Hay algún tipo conveniente que pueda usar para representar esta información?

El tipo más pequeño que he encontrado es un BYTE. Entonces debo usar muchas operaciones binarias para hacer referencia solo a unos pocos bits dentro de esa variable.

avatar de usuario
mmx

Dado que la memoria está direccionada por bytes, no puede direccionar ninguna unidad más pequeña que un solo byte. Sin embargo, puede construir el struct desea enviar a través de la red y utilizar campos de bits como esto:

struct A {
   unsigned int nibble1 : 4;
   unsigned int nibble2 : 4;
};

  • Tenga en cuenta que es importante marcar la variable como “sin firmar”, de lo contrario, el compilador la tratará como firmada y verá números negativos.

    – sean

    14 mayo 2009 a las 14:33

  • Tenga en cuenta que el uso de campos de bits no es necesariamente tan eficiente. Esto era clásicamente un problema; puede ser que los compiladores modernos hayan mejorado. Existe al menos una posibilidad decente de que el enmascaramiento y el cambio sean más rápidos, aunque no necesariamente más claros.

    –Jonathan Leffler

    14 mayo 2009 a las 14:45

  • Esta estructura tiene el tamaño de un ‘int sin firmar’, debido al relleno, al menos aquí en Linux. Entonces, ¿no sería mejor usar ‘char sin firmar’?

    – quinmars

    14 mayo 2009 a las 20:28

  • @quinmars: sugiero decirle al compilador que la estructura está empaquetada en lugar de declararla como unsigned char. En gcc, lo harías agregando atributo__((__empaquetado)) después de la llave de cierre (antes del punto y coma). En MSVC, incluye la declaración de estructura en las directivas #pragma pack (push,1) y #pragma pack (pop) (esto también es compatible con gcc por compatibilidad)

    – mmx

    14 mayo 2009 a las 20:47

  • no es compilador cortar a tajos. Es solo una directiva que le dice al compilador que no agregue relleno innecesario. Lo malo de usar unsigned char es que aún no puede estar seguro de que el compilador específico no agregue relleno para alinearlo con un límite de palabra o palabra clave (si bien es el caso de este ejemplo, podría no serlo para casos más complejos).

    – mmx

    14 mayo 2009 a las 21:29

Ampliando la respuesta de Mehrdad, también use una unión con un byte para evitar algunos moldes malignos:

union Nibbler {
     struct { 
        unsigned int first:4;
        unsigned int second:4;
     } nibbles;
     unsigned char byte_value;
}

  • En su lugar, debe usar caracteres sin firmar en la estructura. De lo contrario, se produce relleno y desperdicia 3 bytes de memoria. Siempre debe intentar hacer coincidir los tipos de estructura con el tipo de unión para evitar el relleno, en una aplicación como esta. Solo agrego esto en caso de que los usuarios no noten los comentarios en la respuesta principal

    – Jack Avante

    2 de noviembre de 2020 a las 9:38


A todos parece gustarles usar campos de bits en structs para esto. Personalmente, envuelvo todo el código de mi paquete en objetos para que no veas las tripas. El problema que encontré con el uso de campos de bits para el código de protocolo es que fomenta el uso de estructuras como superposiciones en la memoria. Puede hacer esto de manera segura, pero debe tener un cuidado insoportable para asegurarse de que está manejando adecuadamente los problemas de endianess y embalaje. A menos que realmente tenga una buena razón (p. ej., está escribiendo el código que recibe el paquete Ethernet de la región IO mapeada en memoria), el uso de campos de bits superpuestos en la memoria produce un código extremadamente frágil en mi humilde opinión.

Me resulta mucho más fácil escribir un Packet clase que implementa rutinas de extracción, inserción y sobrescritura en varios anchos de bits. Luego, implementa su código de procesamiento de paquetes en términos de extraer valores de ciertos anchos de compensaciones en enteros nativos y demás. Oculte todos los problemas de endianess y empaquetado detrás de una abstracción hasta que el perfil demuestre que la sobrecarga es demasiado grande para soportar.

Esta es una de esas lecciones que desearía haber aprendido hace años… podrías pensar que la portabilidad del código no es un problema y tampoco lo es el endianess. Confíe en mí, la cantidad de dolores de cabeza que esto le causa cuando su compilador cambia su algoritmo de relleno o cambia a un compilador diferente lo convencerá de que las superposiciones son una muy mal idea para el código de procesamiento de paquetes de red.

  • El verdadero problema de su solución es que C (el idioma en el que están escritos la mayoría de los controladores de dispositivos) no tiene clases. Deberá recurrir a funciones globales que manejen problemas de conversión. Su punto sobre el relleno es correcto, pero hay una solución: siempre especifique explícitamente los campos de relleno y simplemente dígale al compilador que empaque la estructura

    – mmx

    14 mayo 2009 a las 16:05

  • @Mehrdad, D. Shawley dijo que implementara una clase, pero creo que lo que quiso decir fue crear un nivel más alto de abstracción para deshacerse de los problemas de endianess/bit-twiddling/low-level-byte-ordering/etc. El truco es asegurarse de que su capa adicional de abstracción no incurra en horribles golpes de rendimiento (para este tema, en mi opinión, sería relativamente fácil).

    – Trevor Boyd Smith

    14 mayo 2009 a las 17:29

  • @Mehrdad: la pregunta está etiquetada como C/C++, de ahí surgió la idea de la clase. Si estamos escribiendo en C, entonces Trevor tiene razón: implemente la abstracción utilizando uno de los modismos OOP en C. Recomendaría mirar el código Ethereal/Wireshark como una muy buena implementación de un búfer de bytes. Implementan un Testy Virtual Buffer (TVB) que es bastante similar a lo que yo haría en C++.

    – D. Shawley

    15 mayo 2009 a las 16:50

Usar campos en una estructura:

struct Header
{
    unsigned int lowestNibble : 4;
    unsigned int anotherNibble : 4;
    unsigned int : 18;                 # Unnamed padding.
    bool aBool : 1;
    bool anotherBool : 1;
    unsigned int highestNibble : 4;
};

los : 4 indica que la entrada debe ocupar 4 bits. Puede usar cualquier cantidad de bits que desee. Puede usar cualquier tipo incorporado que desee.

Por lo general, termina lanzando un puntero a sus datos a un Header * luego haciendo algo como:

pHeader->lowestNibble = 5;

No, no hay tipos convenientes para picar. Pero es fácil hacerlos con macros o con funciones de plantilla. Esto funciona bien, especialmente si/cuando necesita lidiar con endian-ness.

proa

¿Ha sido útil esta solución?