读取由结构体定义的二进制文件

3

有人能指导我如何读取由C结构定义的二进制文件吗? 结构体内部有一些#define,这让我觉得会使事情变得复杂。
结构大致如下:(尽管它比这个更大更复杂)

struct Format {
    unsigned long str_totalstrings;
    unsigned long str_name;
    #define STR_ORDERED 0x2
    #define STR_ROT13 0x4
    unsigned char stuff[4];
    #define str_delimiter stuff[0]
}

我希望有人能够指导我如何做到这一点。或者是否有任何涵盖此主题的教程?
非常感谢您提前的帮助。

1
正如AShelly所指出的,#define在结构体中并不重要,但有点奇怪。如果你认为它更易读,那很好。我只想确保你不认为#define在作用域上仅限于结构体,并且只能与结构体一起使用。 - Tom
5个回答

8

有一些不好的想法和好的想法:

这是一个不好的想法:

  • 将原始缓冲区转换为结构体
    • 在解析大于1字节的整数或浮点数时,存在endianness问题(小端 vs 大端)
    • 结构中存在字节对齐问题,这与编译器有关。可以尝试禁用对齐(或强制手动对齐),但通常这也是一个坏主意。至少,您将通过使CPU访问未对齐的整数而破坏性能。内部RISC核心每次访问它都必须执行3-4个操作,而不是1个操作(即“在第一个字中完成第1部分”,“在第二个字中完成第2部分”,“合并结果”)。或者更糟糕的是,编译器指示控制对齐的pragma将被忽略,您的代码将会出错。
    • C/C ++中的常规intlongshort等类型没有确切的大小保证。您可以使用像int16_t这样的东西,但这些仅适用于现代编译器。
    • 当然,如果使用引用其他结构的结构,则此方法将完全失效:必须手动展开它们所有。
  • 手动编写解析器:比第一眼看起来要难得多。
    • 良好的解析器需要在每个阶段进行大量的合理性检查。很容易错过某些东西。如果您不使用异常,那么错过某些东西会更容易。
    • 使用异常使您容易失败,如果您的解析代码不是异常安全的(即以一种可以在某些点中断而不会泄漏内存/忘记完成某些对象的方式编写)
    • 可能存在性能问题(即执行大量未缓冲的IO而不是执行一个OS read系统调用并解析缓冲区,或者反之,一次读取整个内容而不是更粒度化的惰性读取,在适用的情况下)。

建议

  • 跨平台。近年来,随着移动设备、路由器和物联网技术的蓬勃发展,这一点几乎不言自明。
  • 采用声明式。考虑使用任何声明性规范来描述您的结构,然后使用解析器生成器生成解析器。

有几个可用的工具:

  • Kaitai Struct - 我目前最喜欢的,跨平台,跨语言 - 即您只需在一个地方描述您的结构,然后就可以将其编译成C ++,C#,Java,Python,Ruby,PHP等解析器。
  • binpac - 相当陈旧,但仍可用,仅限C ++ - 与Kaitai在思想上类似,但自2013年以来未得到支持。
  • Spicy - 被称为binpac的“现代重写”,也称为“binpac ++”,但仍处于开发的早期阶段;仅适用于较小的任务,仅限于C ++。

4

读取由结构定义的二进制很容易。

Format myFormat;
fread(&myFormat, sizeof(Format), 1, fp);

#defines 不会对结构体产生影响。(不过在结构体内部放置 #defines 是一个奇怪的地方)。

然而,这种做法并不跨平台安全。在确保读写双方使用相同平台的情况下,它是可能起作用的最简单的方式。

更好的方法是重新定义你的结构体:

struct Format {
    Uint32 str_totalstrings;  //assuming unsigned long was 32 bits on the writer.
    Uint32 str_name;
    unsigned char stuff[4];
};

然后有一个名为'platform_types.h'的文件,它为您的编译器正确地定义了Uint32。现在,您可以直接读取结构,但由于字节序问题,仍需执行以下操作:

myFormat.str_totalstrings = FileToNative32(myFormat.str_totalstrings);
myFormat.str_name =   FileToNative32(str_name);

FileToNative是一个根据平台不同而不同的操作,可能是无操作(no-op),也可能是字节反转器。


2
我建议使用sizeof myFormat,而且我认为你漏掉了fread()的一个参数。此外,这假设主机的字节序与写入文件的机器相同。总的来说,在我的看法中,对整个结构进行I/O是一个不好的想法。 - unwind
那种方式有多安全呢?我已经通过读取特定数量的字节并填充结构元素来完成了它。当时有一个原因,但我已经忘记了... - Kevin
2
跨平台和编译器来说这样做并不安全,因为内置类型的实际大小根本没有被标准固定。这样做脆弱且危险。 - Joris Timmermans

2

如果您已经在内存中拥有要解析的数据,您还可以使用联合来进行解析。

union A {
    char* buffer;
    Format format;
};

A a;
a.buffer = stuff_you_want_to_parse;

// You can now access the members of the struct through the union.
if (a.format.str_name == "...")
    // do stuff

同时要记住,long在不同的平台上可能有不同的大小。如果您依赖于long具有特定的大小,请考虑使用在stdint.h中定义的类型,如uint32_t。


1
作为替代方案,我更喜欢使用带有reinterpret_cast的char。例如,取一个char缓冲区并用数据填充它。然后:Format* format = reinterpret_cast<Format*>(buffer); format->str_name = "..."; - Tom
这个问题与我的答案一样,存在跨平台问题。 - AShelly

2
使用C++ I/O库:
#include <fstream>
using namespace std;

ifstream ifs("file.dat", ios::binary);
Format f;
ifs.get(&f, sizeof f);

使用C I/O库:

#include <cstdio>
using namespace std;

FILE *fin = fopen("file.dat", "rb");
Format f;
fread(&f, sizeof f, 1, fin);

1

你需要找出文件所写的机器的字节序,以便正确解释整数。注意 ILP32 与 LP64 不匹配的问题。原始结构的打包/对齐也可能很重要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接