在C中解析二进制数据?

时间:2008-11-26 17:03:01

标签: c parsing network-programming

是否有任何关于如何在C中读取和解析二进制数据的库或指南?

我正在研究一些将在网络套接字上接收TCP数据包然后根据规范解析该二进制数据的功能,并通过代码将信息转换为更有用的形式。

是否有任何图书馆可以做到这一点,甚至还有关于执行此类事情的入门书?

9 个答案:

答案 0 :(得分:29)

我不得不同意这里的许多回应。我强烈建议你避免将结构转换为传入数据的诱惑。它似乎很有吸引力,甚至可能适用于您当前的目标,但如果代码被移植到另一个目标/环境/编译器,您将遇到麻烦。原因如下:

Endianness :您现在使用的架构可能是big-endian,但您的下一个目标可能是little-endian。或相反亦然。您可以使用宏(例如ntoh和hton)来克服这个问题,但这是额外的工作,并确保每次引用该字段时都调用这些宏

对齐:您正在使用的架构可能能够在奇数寻址偏移处加载多字节字,但许多架构不能。如果一个4字节的字跨越一个4字节的对齐边界,那么负载可能会产生垃圾。即使协议本身没有未对齐的字,有时字节流本身也是未对齐的。 (例如,虽然IP头定义将所有4字节字放在4字节边界上,但通常以太网头将IP头本身推送到2字节边界。)

填充:您的编译器可能会选择紧密打包您的结构而不填充,或者它可能会插入填充以处理目标的对齐约束。我在同一个编译器的两个版本之间看到了这种变化。您可以使用#pragmas强制解决问题,但#pragmas当然是特定于编译器的。

位排序:C位域内的位排序是特定于编译器的。另外,这些位很难为运行时代码“获取”。每次在结构中引用位域时,编译器都必须使用一组掩码/移位操作。当然,你将不得不在某些时候进行掩蔽/移动,但如果速度是一个问题,最好不要在每次参考时都这样做。 (如果空间是最重要的考虑因素,那么请使用位域,但要谨慎行事。)

这一切并不是说“不要使用结构”。我最喜欢的方法是在没有任何位域的情况下声明所有相关协议数据的友好的native-endian结构而不关心问题,然后编写一组使用struct作为中间人的对称打包/解析例程。

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!  :)
}

现在,您的其余代码只是在友好,快速的struct对象中操作数据,并且只在必须与字节流接口时才调用pack / parse。不需要ntoh或hton,也没有位域来减慢你的代码。

答案 1 :(得分:14)

在C / C ++中执行此操作的标准方法是将结构强制转换为'gwaredd'建议

它并不像人们想象的那样不安全。您首先转换为您期望的结构,如在他/她的示例中,然后您测试该结构的有效性。您必须测试最大/最小值,终止序列等。

您所处的平台必须阅读Unix Network Programming, Volume 1: The Sockets Networking API。买它,借它,偷它(受害者会理解,这就像偷食物或东西......),但要读它。

在阅读史蒂文斯之后,大部分内容都会更有意义。

答案 2 :(得分:12)

让我重申你的问题,看看我是否理解得当。你是 寻找能够正式描述数据包的软件 然后会产生一个“解码器”来解析这些数据包吗?

如果是,则该字段中的引用为PADS。一篇好文章 介绍它是PADS: A Domain-Specific Language for Processing Ad Hoc Data。 PADS非常完整,但遗憾的是非自由许可证。

有可能的选择(我没有提到非C 解决方案)。显然,没有一个可以被视为完全生产就绪:

如果你读法语,我在Génération de décodeurs de formats binaires中总结了这些问题。

答案 3 :(得分:10)

根据我的经验,最好的方法是首先编写一组原语,从二进制缓冲区读取/写入某种类型的单个值。这为您提供了高可见性,以及处理任何字节序问题的非常简单的方法:只需使函数正确执行。

然后,您可以为每个协议消息定义struct,并为每个消息写入pack / unpack(有些人称之为序列化/反序列化)函数。

作为基本情况,提取单个8位整数的原语可能如下所示(假设主机上有8位char,您可以添加一层自定义类型以确保它也是如此,如果需要):

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

在这里,我选择通过引用返回值,并返回更新的指针。这是一个品味问题,您当然可以返回值并通过引用更新指针。读取函数更新指针,使这些链接成为设计的关键部分。

现在,我们可以编写一个类似的函数来读取16位无符号数量:

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;
}

这里我假设传入数据是big-endian,这在网络协议中很常见(主要是出于历史原因)。你当然可以聪明地做一些指针算法并且不需要临时,但我发现这种方式使它更清晰,更容易理解。在调试时,这种原语具有最大透明度是一件好事。

下一步是开始定义特定于协议的消息,并编写读/写原语以进行匹配。在这个级别,考虑代码生成;如果您的协议以一般的机器可读格式描述,您可以从中生成读/写功能,这可以节省很多麻烦。如果协议格式为clever enough,则更难,但通常可行且强烈推荐。

答案 4 :(得分:5)

您可能对Google Protocol Buffers感兴趣,它基本上是一个序列化框架。它主要用于C ++ / Java / Python(这些是Google支持的语言),但一直在努力将其移植到其他语言,包括C。 (我根本没有使用过C端口,但我负责其中一个C#端口。)

答案 5 :(得分:3)

你真的不需要在C中解析二进制数据,只需要将一些指针转换为你认为它应该是什么。

struct SomeDataFormat
{
    ....
}

SomeDataFormat* pParsedData = (SomeDataFormat*) pBuffer;

警惕端点问题,类型大小,读取缓冲区末尾等等

答案 6 :(得分:2)

解析/格式化二进制结构是在C中比在更高级别/托管语言中更容易做的极少数事情之一。您只需定义一个与您要处理的格式相对应的结构,而 是解析器/格式化程序。这是有效的,因为C中的结构表示精确的内存布局(当然,它已经是二进制)。另见kervin和gwaredd的回复。

答案 7 :(得分:1)

我真的不明白你在找什么样的图书馆?将采用任何二进制输入并将其解析为未知格式的通用库? 我不确定任何语言都可以存在这样的库。 我想你需要详细说明你的问题。

修改
好的,所以在阅读Jon's后,答案似乎有一个库,很好的库,它更像是代码生成工具。但是,正如许多人所说,只是将数据转换为适当的数据结构,并且要谨慎,即使用打包结构并处理字节序问题,这样做很好。使用C这样的工具只是一种矫枉过正。

答案 8 :(得分:1)

关于转换为struct工作的基本建议,但请注意,不同架构上的数字可以表示不同。

为了处理字节序问题,引入了网络字节顺序 - 通常的做法是在发送数据之前将数字从主机字节顺序转换为网络字节顺序,并在接收时转换回主机顺序。查看函数htonlhtonsntohlntohs

真的考虑科尔文的建议 - 阅读UNP。你不会后悔!