¿Análisis de datos binarios en C?

11 minutos de lectura

¿Existen bibliotecas o guías sobre cómo leer y analizar datos binarios en C?

Estoy viendo alguna funcionalidad que recibirá paquetes TCP en un socket de red y luego analizará esos datos binarios de acuerdo con una especificación, convirtiendo la información en una forma más útil para el código.

¿Hay alguna biblioteca por ahí que haga esto, o incluso un manual básico sobre cómo realizar este tipo de cosas?

avatar de usuario
Casey Barker

Tengo que estar en desacuerdo con muchas de las respuestas aquí. Le sugiero encarecidamente que evite la tentación de crear una estructura en los datos entrantes. Parece convincente e incluso podría funcionar en su objetivo actual, pero si el código alguna vez se transfiere a otro objetivo/entorno/compilador, se encontrará con problemas. Algunas razones:

endianidad: La arquitectura que está utilizando en este momento puede ser big-endian, pero su próximo objetivo podría ser little-endian. O viceversa. Puede superar esto con macros (ntoh y hton, por ejemplo), pero es un trabajo adicional y debe asegurarse de llamar a esas macros cada vez usted hace referencia al campo.

Alineación: La arquitectura que está utilizando podría ser capaz de cargar una palabra de varios bytes en un desplazamiento de direcciones impares, pero muchas arquitecturas no pueden. Si una palabra de 4 bytes se extiende a ambos lados de un límite de alineación de 4 bytes, la carga puede generar basura. Incluso si el protocolo en sí no tiene palabras desalineadas, a veces el flujo de bytes en sí mismo está desalineado. (Por ejemplo, aunque la definición del encabezado IP coloca todas las palabras de 4 bytes en límites de 4 bytes, a menudo el encabezado de Ethernet empuja el encabezado IP en un límite de 2 bytes).

Relleno: Su compilador puede optar por empaquetar su estructura de forma ajustada sin relleno, o puede insertar relleno para lidiar con las restricciones de alineación del objetivo. He visto este cambio entre dos versiones del mismo compilador. Podría usar #pragmas para forzar el problema, pero los #pragmas son, por supuesto, específicos del compilador.

Ordenación de bits: El orden de los bits dentro de los campos de bits de C es específico del compilador. Además, los bits son difíciles de “obtener” para su código de tiempo de ejecución. Cada vez que hace referencia a un campo de bits dentro de una estructura, el compilador tiene que usar un conjunto de operaciones de máscara/cambio. Por supuesto, tendrá que enmascarar/cambiar en algún momento, pero es mejor no hacerlo en cada referencia si la velocidad es una preocupación. (Si el espacio es la principal preocupación, utilice campos de bits, pero tenga cuidado).

Todo esto no quiere decir “no use estructuras”. Mi enfoque favorito es declarar una estructura nativa-endiana amigable de todos los datos de protocolo relevantes sin ningún campo de bits y sin preocuparse por los problemas, luego escribir un conjunto de rutinas de análisis/paquete simétrico que usan la estructura como intermediario.

typedef struct _MyProtocolData
{
    Bool myBitA;  // Using a "Bool" type wastes a lot of space, but it's fast.
    Bool myBitB;
    Word32 myWord;  // You have a list of base types like Word32, right?
} MyProtocolData;

Void myProtocolParse(const Byte *pProtocol, MyProtocolData *pData)
{
    // Somewhere, your code has to pick out the bits.  Best to just do it one place.
    pData->myBitA = *(pProtocol + MY_BITS_OFFSET) & MY_BIT_A_MASK >> MY_BIT_A_SHIFT;
    pData->myBitB = *(pProtocol + MY_BITS_OFFSET) & MY_BIT_B_MASK >> MY_BIT_B_SHIFT;

    // Endianness and Alignment issues go away when you fetch byte-at-a-time.
    // Here, I'm assuming the protocol is big-endian.
    // You could also write a library of "word fetchers" for different sizes and endiannesses.
    pData->myWord  = *(pProtocol + MY_WORD_OFFSET + 0) << 24;
    pData->myWord += *(pProtocol + MY_WORD_OFFSET + 1) << 16;
    pData->myWord += *(pProtocol + MY_WORD_OFFSET + 2) << 8;
    pData->myWord += *(pProtocol + MY_WORD_OFFSET + 3);

    // You could return something useful, like the end of the protocol or an error code.
}

Void myProtocolPack(const MyProtocolData *pData, Byte *pProtocol)
{
    // Exercise for the reader!  :)
}

Ahora, el resto de su código solo manipula los datos dentro de los objetos de estructura amigables y rápidos y solo llama al paquete/análisis cuando tiene que interactuar con un flujo de bytes. No hay necesidad de ntoh o hton, y no hay campos de bits para ralentizar su código.

  • ¿Funciona este código incluso para pasar una estructura a través de sockets >>>

    – fanático de la codificación

    20 de octubre de 2009 a las 9:55

  • Es expresamente bueno para los sockets, especialmente cuando no desea hacer afirmaciones sobre el endianness/ancho del bus/alineación de los procesos en cualquiera de los extremos del socket.

    –Casey Barker

    3 de noviembre de 2009 a las 15:33

  • Estoy completamente de acuerdo con sus comentarios, pero el código en sí debería haber sido más explícito al respecto. La parte en la que convierte los bytes sin procesar en una palabra se debe hacer usando una instancia de algún convertidor de Endian, de modo que se pueda cambiar fácilmente con una implementación diferente cuando sea necesario.

    – Groo

    25 de febrero de 2011 a las 9:57


  • De ahí mi comentario de código “También podría escribir una biblioteca de” buscadores de palabras “para diferentes tamaños y endianness”. Entonces, ¿quieres que escriba toda la biblioteca? 🙂 Pero este código funciona en cualquier núcleo de cualquier orientación endian. Es completamente portátil, siempre que escriba correctamente “Byte” y “Word32”, por lo que no veo por qué necesitaría “cambiar la implementación”.

    –Casey Barker

    18 de mayo de 2011 a las 17:02


  • Esta pregunta volvió a surgir recientemente y, al volver a leerla, creo que Groo no entendió el punto de esta implementación. Entonces, quiero decir más específicamente: este código funciona para CUALQUIER procesador de CUALQUIER ancho o endian, siempre que el PROTOCOLO sea big-endian. Los protocolos no suelen cambiar después de que se definen, por lo que realmente no es un problema.

    –Casey Barker

    24 de agosto de 2011 a las 19:06

avatar de usuario
Kervin

La forma estándar de hacer esto en C/C++ es realmente convertir a estructuras como sugiere ‘gwaredd’

No es tan inseguro como uno pensaría. Primero lanzas a la estructura que esperabas, como en su ejemplo, después usted prueba esa estructura para la validez. Tienes que probar los valores máximos/mínimos, las secuencias de terminación, etc.

Independientemente de la plataforma en la que se encuentre, debe leer Programación de redes Unix, Volumen 1: La API de redes de sockets. Cómpralo, tómalo prestado, róbalo (la víctima lo entenderá, es como robar comida o algo así…), pero léelo.

Después de leer Stevens, la mayor parte de esto tendrá mucho más sentido.

  • Soy escéptico sobre el método “lanzar y luego verificar”. Si no marca, corre el riesgo de obtener datos no válidos. Y si marca, ¿cuál es el punto de lanzar? La comprobación será tan lenta como el análisis tradicional.

    – bortzmeyer

    27 de noviembre de 2008 a las 7:42

  • Como escribió Casey Barker a continuación, las cosas no son tan simples. Puede corregir la alineación de bytes y el relleno la mayor parte del tiempo (y debe ser consciente de esto y probarlo a fondo con cada nuevo sistema), pero una vez que tenga problemas de orden endian, se verá obligado a corregir cada estructura individualmente antes de verificar para la validez. Y si está comprobando la validez, también puede comprobarlo durante el análisis. El análisis de tokens individuales también permite la creación de subclases y versiones detalladas.

    – Groo

    25 de febrero de 2011 a las 9:53

  • De hecho, la Validación de archivos de Office se introdujo en Office 2010 y luego se adaptó a Office 2007 y Office 2003 básicamente verifica la validez del archivo para evitar que se exploten las vulnerabilidades.

    –Yuhong Bao

    6 de agosto de 2011 a las 21:33

  • El enlace del mezclador parece estar muerto desde 2011. Parece que hay una copia válida aquí: csis.bits-pilani.ac.in/faculty/dk_tyagi/Study_stuffs/raw.html

    – Tik Tok

    22 de julio de 2015 a las 16:50

Déjame repetir tu pregunta para ver si entendí bien. ¿Está buscando un software que tome una descripción formal de un paquete y luego produzca un “decodificador” para analizar dichos paquetes?

Si es así, la referencia en ese campo es ALMOHADILLAS. Un buen artículo que lo presenta es PADS: un lenguaje específico de dominio para procesar datos ad hoc. PADS es muy completo pero desafortunadamente bajo una licencia no libre.

Hay posibles alternativas (no mencioné soluciones que no sean C). Aparentemente, ninguno puede considerarse completamente listo para la producción:

Si lees francés, resumí estos temas en Generación de codificadores de formatos binarios.

  • @bortzmeyer Todas estas son noticias para mí. Gracias por la info!

    -Bklyn

    13 de abril de 2009 a las 19:41

En mi experiencia, la mejor manera es escribir primero un conjunto de primitivas, para leer/escribir un solo valor de algún tipo desde un búfer binario. Esto le brinda una alta visibilidad y una forma muy simple de manejar cualquier problema de endianness: simplemente haga que las funciones lo hagan bien.

Luego, puede, por ejemplo, definir structs para cada uno de sus mensajes de protocolo, y escriba funciones de empaquetado/desempaquetado (algunas personas las llaman serializar/deserializar) para cada uno.

Como caso base, una primitiva para extraer un solo entero de 8 bits podría tener este aspecto (suponiendo que un número entero de 8 bits char en la máquina host, puede agregar una capa de tipos personalizados para garantizar eso también, si es necesario):

const void * read_uint8(const void *buffer, unsigned char *value)
{
  const unsigned char *vptr = buffer;
  *value = *buffer++;
  return buffer;
}

Aquí, elegí devolver el valor por referencia y devolver un puntero actualizado. Esto es cuestión de gustos, por supuesto, puede devolver el valor y actualizar el puntero por referencia. Es una parte crucial del diseño que la función de lectura actualice el puntero, para hacerlos encadenables.

Ahora, podemos escribir una función similar para leer una cantidad sin signo de 16 bits:

const void * read_uint16(const void *buffer, unsigned short *value)
{
  unsigned char lo, hi;

  buffer = read_uint8(buffer, &hi);
  buffer = read_uint8(buffer, &lo);
  *value = (hi << 8) | lo;
  return buffer;
}

Aquí asumí que los datos entrantes son big-endian, esto es común en los protocolos de red (principalmente por razones históricas). Por supuesto, podría ser inteligente y hacer algo de aritmética de punteros y eliminar la necesidad de un temporal, pero creo que de esta manera lo hace más claro y más fácil de entender. Tener la máxima transparencia en este tipo de primitiva puede ser algo bueno al depurar.

El siguiente paso sería comenzar a definir los mensajes específicos de su protocolo y escribir primitivas de lectura/escritura para que coincidan. En ese nivel, piense en la generación de código; si su protocolo se describe en algún formato general legible por máquina, puede generar las funciones de lectura/escritura a partir de eso, lo que ahorra mucho dolor. Esto es más difícil si el formato del protocolo es suficientemente listopero a menudo factible y muy recomendable.

Te podría interesar Búferes de protocolo de Google, que es básicamente un marco de serialización. Es principalmente para C++/Java/Python (esos son los idiomas admitidos por Google), pero se están realizando esfuerzos para trasladarlo a otros idiomas, incluidos C. (No he usado el puerto C en absoluto, pero soy responsable de uno de los puertos C#).

  • Hay muchas formas de serializar datos (Protocol Buffers está bien, pero es solo una de ellas, también hay XML, JSON, ASN/1+BER, etc.). Funcionan solo si controlas la especificación del protocolo. Si no es el caso, su método no funciona.

    – bortzmeyer

    27 de noviembre de 2008 a las 7:44

  • Absolutamente. Si no tiene el control del protocolo, básicamente tiene que hacerlo manualmente.

    – Jon Skeet

    27 de noviembre de 2008 a las 8:11

avatar de usuario
Gwardd

Realmente no necesita analizar datos binarios en C, solo envíe un puntero a lo que crea que debería ser.

struct SomeDataFormat
{
    ....
}

SomeDataFormat* pParsedData = (SomeDataFormat*) pBuffer;

Solo tenga cuidado con los problemas endian, los tamaños de letra, la lectura del final de los búferes, etc., etc.

  • Hay muchas formas de serializar datos (Protocol Buffers está bien, pero es solo una de ellas, también hay XML, JSON, ASN/1+BER, etc.). Funcionan solo si controlas la especificación del protocolo. Si no es el caso, su método no funciona.

    – bortzmeyer

    27 de noviembre de 2008 a las 7:44

  • Absolutamente. Si no tiene el control del protocolo, básicamente tiene que hacerlo manualmente.

    – Jon Skeet

    27 de noviembre de 2008 a las 8:11

avatar de usuario
mate campbell

Analizar/formatear estructuras binarias es una de las muy pocos cosas que son más fáciles de hacer en C que en lenguajes de nivel superior/administrados. Simplemente define una estructura que corresponde al formato que desea manejar y la estructura es el analizador/formateador. Esto funciona porque una estructura en C representa un diseño de memoria preciso (que, por supuesto, ya es binario). Vea también las respuestas de Kervin y Gwaredd.

¿Ha sido útil esta solución?