解析二进制文件。什么是现代方式?

时间:2014-11-10 14:00:27

标签: c++ casting binary

我有一个二进制文件,我知道一些布局。例如,让格式如下:

  • 2个字节(无符号短) - 字符串的长度
  • 5个字节(5个字符) - 字符串 - 一些id名称
  • 4个字节(unsigned int) - 一个步幅
  • 24个字节(6个浮点数 - 每个3个浮点数的2个步幅) - 浮点数据

文件看起来应该是(为了便于阅读,我添加了空格):

5 hello 3 0.0 0.1 0.2 -0.3 -0.4 -0.5

这里5 - 是2个字节:0x05 0x00。 "你好" - 5个字节,依此类推。

现在我想读这个文件。目前我是这样做的:

  • 将文件加载到 ifstream
  • 将此信息流读取至char buffer[2]
  • 将其投放到无符号短片:unsigned short len{ *((unsigned short*)buffer) };。现在我有一个字符串的长度。
  • 读取vector<char>的流并从此向量创建std::string。现在我有字符串ID。
  • 以相同的方式读取接下来的4个字节并将它们转换为unsigned int。现在我有了一个步伐。
  • 虽然不是文件读取结束以相同的方式浮动 - 为每个浮动创建char bufferFloat[4]并投射*((float*)bufferFloat)

这有效,但对我来说它看起来很难看。我可以直接阅读unsigned shortfloatstring等,而不会char [x]创建吗?如果不是,那么正确投射的方法是什么(我读过我使用的那种风格 - 是旧风格)?

P.S。:当我写一个问题时,我头脑中提出的解释越清楚 - 如何从char [x]的任意位置投出任意数量的字节?

更新:我忘了明确提到字符串和浮点数据长度在编译时是未知的并且是可变的。

11 个答案:

答案 0 :(得分:13)

如果不是出于学习目的,并且您可以自由选择二进制格式,那么最好考虑使用 protobuf 之类的内容来处理序列化并允许与其他平台和语言互操作。

如果您无法使用第三方API,可以查看QDataStream获取灵感

答案 1 :(得分:9)

C语言在C ++中运行良好,就是声明一个结构:

#pragma pack(1)

struct contents {
   // data members;
};

请注意

  • 您需要使用编译指示使编译器在结构中对齐 as-it-looks 数据;
  • 此技术仅适用于POD types

然后将读缓冲区直接转换为struct类型:

std::vector<char> buf(sizeof(contents));
file.read(buf.data(), buf.size());
contents *stuff = reinterpret_cast<contents *>(buf.data());

现在,如果您的数据大小是可变的,您可以分成几个块。要从缓冲区中读取单个二进制对象,读取器功能非常方便:

template<typename T>
const char *read_object(const char *buffer, T& target) {
    target = *reinterpret_cast<const T*>(buffer);
    return buffer + sizeof(T);
}

主要优点是这样的阅读器可以专门用于更高级的c ++对象:

template<typename CT>
const char *read_object(const char *buffer, std::vector<CT>& target) {
    size_t size = target.size();
    CT const *buf_start = reinterpret_cast<const CT*>(buffer);
    std::copy(buf_start, buf_start + size, target.begin());
    return buffer + size * sizeof(CT);
}

现在在你的主解析器中:

int n_floats;
iter = read_object(iter, n_floats);
std::vector<float> my_floats(n_floats);
iter = read_object(iter, my_floats);

注意:正如Tony D观察到的那样,即使您可以通过#pragma指令和手动填充(如果需要)进行对齐,您仍可能会遇到与处理器不兼容的问题。 ;对齐,以(最佳情况)性能问题或(最坏情况)陷阱信号的形式。只有在您控制文件的格式时,此方法才有意义。

答案 2 :(得分:9)

  

目前我这样做:

     
      
  • 将文件加载到ifstream

  •   
  • 将此流读取到char缓冲区[2]

  •   
  • 将其投放到unsigned shortunsigned short len{ *((unsigned short*)buffer) };。现在我有一个字符串的长度。

  •   

最后冒险SIGBUS(如果您的字符阵列恰好从奇数地址开始,而您的CPU只能读取在偶数地址对齐的16位值),性能(某些CPU将读取未对齐)值,但速度较慢;其他像现代x86一样好,速度快)和/或endianness问题。我建议你阅读这两个字符然后你可以说(x[0] << 8) | x[1],反之亦然,如果需要纠正字节顺序,可以使用htons

  
      
  • 读取vector<char>的信息流并从此std::string创建vector。现在我有字符串ID。
  •   

不需要......只需直接读入字符串:

std::string s(the_size, ' ');

if (input_fstream.read(&s[0], s.size()) &&
    input_stream.gcount() == s.size())
    ...use s...
  
      
  • 以相同的方式read接下来的4个字节并将它们转换为unsigned int。现在我有一个进步。   while文件read float的结尾方式相同 - 为char bufferFloat[4]创建*((float*)bufferFloat)并投放float
  •   

最好直接通过unsigned intfloats读取数据,因为编译器将确保正确对齐。

  

这有效,但对我来说它看起来很难看。我可以直接阅读unsigned shortfloatstring等,而不会char [x]创建吗?如果不是,那么正确投射的方法是什么(我读过我使用的那种风格 - 是旧风格)?

struct Data
{
    uint32_t x;
    float y[6];
};
Data data;
if (input_stream.read((char*)&data, sizeof data) &&
    input_stream.gcount() == sizeof data)
    ...use x and y...

请注意,上面的代码可以避免将数据读入可能未对齐的字符数组中,因为它对可能未对齐的reinterpret_cast数组中的char数据不安全(包括std::string内部)由于对齐问题。同样,如果文件内容的字节顺序不同,您可能需要使用htonl进行一些读后转换。如果float s的数量未知,您需要计算并分配至少4个字节对齐的足够存储空间,然后将Data*对准...只要访问的地址中的内存内容是分配的一部分并保持从流中读入的有效y表示,索引超过声明的数组大小float是合法的。更简单 - 但附加读取可能更慢 - 请首先阅读uint32_t然后再new float[n]再阅读read ....

实际上,这种方法可以工作,很多低级和C代码都可以做到这一点。 &#34;清洁&#34;可能帮助您阅读文件的高级库最终必须在内部执行类似的操作....

答案 3 :(得分:6)

我实际上在上个月实现了一个快速且脏的二进制格式解析器来读取.zip文件(遵循维基百科的格式描述),并且现代化我决定使用C ++模板。

在某些特定平台上,打包的struct可以正常工作,但有些事情处理得不好......例如可变长度的字段。但是,对于模板,没有这样的问题:您可以获得任意复杂的结构(和返回类型)。

幸运的是,.zip存档相对简单,所以我实现了一些简单的操作。在我的头顶:

using Buffer = std::pair<unsigned char const*, size_t>;

template <typename OffsetReader>
class UInt16LEReader: private OffsetReader {
public:
    UInt16LEReader() {}
    explicit UInt16LEReader(OffsetReader const or): OffsetReader(or) {}

    uint16_t read(Buffer const& buffer) const {
        OffsetReader const& or = *this;

        size_t const offset = or.read(buffer);
        assert(offset <= buffer.second && "Incorrect offset");
        assert(offset + 2 <= buffer.second && "Too short buffer");

        unsigned char const* begin = buffer.first + offset;

        // http://commandcenter.blogspot.fr/2012/04/byte-order-fallacy.html
        return (uint16_t(begin[0]) << 0)
             + (uint16_t(begin[1]) << 8);
    }
}; // class UInt16LEReader

// Declined for UInt[8|16|32][LE|BE]...

当然,基本OffsetReader实际上有一个恒定的结果:

template <size_t O>
class FixedOffsetReader {
public:
    size_t read(Buffer const&) const { return O; }
}; // class FixedOffsetReader

由于我们正在讨论模板,您可以在闲暇时切换类型(您可以实现一个代理阅读器,将所有读取委托给记住它们的shared_ptr

但有趣的是最终结果:

// http://en.wikipedia.org/wiki/Zip_%28file_format%29#File_headers
class LocalFileHeader {
public:
    template <size_t O>
    using UInt32 = UInt32LEReader<FixedOffsetReader<O>>;
    template <size_t O>
    using UInt16 = UInt16LEReader<FixedOffsetReader<O>>;

    UInt32< 0> signature;
    UInt16< 4> versionNeededToExtract;
    UInt16< 6> generalPurposeBitFlag;
    UInt16< 8> compressionMethod;
    UInt16<10> fileLastModificationTime;
    UInt16<12> fileLastModificationDate;
    UInt32<14> crc32;
    UInt32<18> compressedSize;
    UInt32<22> uncompressedSize;

    using FileNameLength = UInt16<26>;
    using ExtraFieldLength = UInt16<28>;

    using FileName = StringReader<FixedOffsetReader<30>, FileNameLength>;

    using ExtraField = StringReader<
        CombinedAdd<FixedOffsetReader<30>, FileNameLength>,
        ExtraFieldLength
    >;

    FileName filename;
    ExtraField extraField;
}; // class LocalFileHeader

这显然相当简单,但同时又非常灵活。

一个明显的改进轴将是改善链接,因为这里存在意外重叠的风险。我的档案阅读代码在我第一次尝试时工作,这足以证明这个代码足以完成手头的任务。

答案 4 :(得分:3)

我必须解决这个问题一次。数据文件打包为FORTRAN输出。对齐都错了。我成功地使用了预处理器技巧,自动完成了手动操作:将原始数据从字节缓冲区解压缩到结构。我们的想法是在包含文件中描述数据:

BEGIN_STRUCT(foo)
    UNSIGNED_SHORT(length)
    STRING_FIELD(length, label)
    UNSIGNED_INT(stride)
    FLOAT_ARRAY(3 * stride)
END_STRUCT(foo)

现在你可以定义这些宏来生成你需要的代码,比如struct声明,包括上面的,undef并再次定义宏来生成解包函数,然后是另一个include等。

NB我第一次看到gcc中用于抽象语法树相关代码生成的这种技术。

如果CPP不够强大(或者这种预处理器滥用不适合你),请替换一个小的lex / yacc程序(或选择你喜欢的工具)。

令我感到惊讶的是,在生成代码而不是手工编写代码方面需要多少考虑,至少在像这样的低级基础代码中。

答案 5 :(得分:2)

你应该更好地声明一个结构(使用1字节填充 - 如何 - 取决于编译器)。使用该结构编写,并使用相同的结构读取。仅将POD置于结构中,因此不会std::string等。仅将此结构用于文件I / O或其他进程间通信 - 使用普通structclass来保留它进一步用于C ++程序。

答案 6 :(得分:2)

由于您的所有数据都是可变的,您可以单独阅读这两个块,但仍然使用强制转换:

struct id_contents
{
    uint16_t len;
    char id[];
} __attribute__((packed)); // assuming gcc, ymmv

struct data_contents
{
    uint32_t stride;
    float data[];
} __attribute__((packed)); // assuming gcc, ymmv

class my_row
{
    const id_contents* id_;
    const data_contents* data_;
    size_t len;

public:
    my_row(const char* buffer) {
        id_= reinterpret_cast<const id_contents*>(buffer);
        size_ = sizeof(*id_) + id_->len;
        data_ = reinterpret_cast<const data_contents*>(buffer + size_);
        size_ += sizeof(*data_) + 
            data_->stride * sizeof(float); // or however many, 3*float?

    }

    size_t size() const { return size_; }
};

这样你就可以使用kbok先生的答案来正确解析:

const char* buffer = getPointerToDataSomehow();

my_row data1(buffer);
buffer += data1.size();

my_row data2(buffer);
buffer += data2.size();

// etc.

答案 7 :(得分:2)

我个人这样做:

// some code which loads the file in memory
#pragma pack(push, 1)
struct someFile { int a, b, c; char d[0xEF]; };
#pragma pack(pop)

someFile* f = (someFile*) (file_in_memory);
int filePropertyA = f->a;

文件开头的固定大小结构的非常有效的方法。

答案 8 :(得分:1)

使用序列化库。以下是一些:

答案 9 :(得分:0)

我使用ragel工具为具有1-2K RAM的微控制器生成纯C程序源代码(无表)。它不使用任何文件进行缓冲,并使用状态机图生成易于调试的代码和.dot / .pdf文件。

ragel还可以输出go,Java,..代码进行解析,但是我没有使用这些功能。

ragel的主要功能是能够解析任何字节构建数据,但是您不能挖掘位字段。另一个问题是ragel能够解析常规结构,但没有递归和语法语法解析。

答案 10 :(得分:0)

Kaitai Struct 库提供了一种非常有效的声明式方法,它具有跨编程语言工作的额外好处。

installing the compiler 之后,您需要创建一个 .ksy 文件来描述二进制文件的布局。对于您的情况,它看起来像这样:

# my_type.ksy

meta:
  id: my_type
  endian: be # for big-endian, or "le" for little-endian

seq: # describes the actual sequence of data one-by-one
  - id: len
    type: u2 # unsigned short in C++, two bytes
  - id: my_string
    type: str
    size: 5
    encoding: UTF-8
  - id: stride
    type: u4 # unsigned int in C++, four bytes
  - id: float_data
    type: f4 # a four-byte floating point number
    repeat: expr
    repeat-expr: 6 # repeat six times

然后您可以使用 kaitai struct compiler .ksy 编译 ksc 文件:

# wherever the compiler is installed
# -t specifies the target language, in this case C++
/usr/local/bin/kaitai-struct-compiler my_type.ksy -t cpp_stl

这将创建一个 my_type.cpp 文件和一个 my_type.h 文件,然后您可以将其包含在您的 C++ 代码中:


#include <fstream>
#include <kaitai/kaitaistream.h>
#include "my_type.h"

int main()
{
  std::ifstream ifs("my_data.bin", std::ifstream::binary);
  kaitai::kstream ks(&ifs);
  my_type_t obj(&ks);

  std::cout << obj.len() << '\n'; // you can now access properties of the object

  return 0;
}

希望这有帮助!您可以找到 Kaitai Struct here 的完整文档。它有许多其他功能,是一般二进制解析的绝佳资源。