解析二进制文件.什么是现代方式
Parsing a binary file. What is a modern way?
我有一个二进制文件,我知道有一些布局。例如,让格式如下所示:
- 2 字节(无符号短( - 字符串的长度 5
- 字节(5 x 字符(- 字符串 - 某个 ID 名称
- 4 字节(无符号整数(- 步幅
- 24 字节(6 x 浮点数 - 2 步,每步 3 个浮点数( - 浮点数据
该文件应如下所示(我添加了空格以提高可读性(:
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 个字节并将它们转换为无符号的 int。现在我大步前进了。
- 虽然不是文件结束读取以相同的方式浮动 - 为每个浮点数创建一个
char bufferFloat[4]
并强制转换*((float*)bufferFloat)
。
这有效,但对我来说它看起来很丑。我可以在不char [x]
创建的情况下直接阅读unsigned short
或float
或string
等吗?如果不是,正确投射的方法是什么(我阅读了我正在使用的样式 - 是旧样式(?
PS:当我写一个问题时,我脑海中提出了更清晰的解释 - 如何从char [x]
中的任意位置投射任意数量的字节?
更新:我忘了明确提到字符串和浮点数据长度在编译时是未知的,并且是可变的。
如果不是为了学习目的,并且如果你有选择二进制格式的自由,你最好考虑使用像protobuf这样的东西,它将为你处理序列化,并允许与其他平台和语言进行互操作。
如果您无法使用第三方 API,您可以查看QDataStream
以获取灵感
- 文档
- 源代码
C 方法在 C++ 中可以正常工作,是声明一个结构:
#pragma pack(1)
struct contents {
// data members;
};
请注意,
- 您需要使用杂注使编译器按结构中的外观对齐数据;
- 此技术仅适用于 POD 类型
然后将读取缓冲区直接转换为结构类型:
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
指令和手动填充(如果需要(获得正确的对齐方式,您仍然可能会遇到与处理器对齐不兼容的情况,表现为(最佳情况(性能问题或(最坏情况(陷阱信号。仅当您可以控制文件格式时,此方法才可能很有趣。
目前我是这样做的:
将文件加载到 ifstream
将此流读取到字符缓冲区[2]
投
unsigned short
:unsigned short len{ *((unsigned short*)buffer) };
.现在我有一个字符串的长度。
最后一个风险是SIGBUS
(如果您的字符数组碰巧从一个奇数地址开始,并且您的 CPU 只能读取在偶数地址对齐的 16 位值(、性能(一些 CPU 会读取未对齐的值但速度较慢;其他像现代 x86 一样很好而且很快(和/或字节序问题。 我建议阅读这两个字符,然后你可以说(x[0] << 8) | x[1]
反之亦然,如果需要纠正字节序,请使用htons
。
- 读取要
vector<char>
的流并从此vector
创建std::string
。现在我有字符串 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
强制转换*((float*)bufferFloat)
。
最好直接通过unsigned int
和floats
读取数据,因为这样编译器将确保正确对齐。
这有效,但对我来说它看起来很丑。我可以在不
char [x]
创建的情况下直接阅读unsigned short
或float
或string
等吗?如果不是,正确投射的方法是什么(我阅读了我正在使用的样式 - 是旧样式(?
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
的数量未知,则需要计算并分配足够的存储空间,至少对齐 4 个字节,然后瞄准它Data*
......只要访问地址处的内存内容是分配的一部分,并且包含从流中读入的有效float
表示形式,就索引超过 y
声明的数组大小是合法的。 更简单 - 但额外的读取可能会更慢 - 先阅读uint32_t
,然后new float[n]
并进一步read
那里......
实际上,这种类型的方法可以工作,并且许多低级和C代码正是这样做的。 可以帮助您读取文件的"更干净"的高级库最终必须在内部执行类似操作。
上个月,我实际上实现了一个快速而肮脏的二进制格式解析器来读取.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
显然,这相当简单,但同时又非常灵活。
一个明显的改进轴是改进链接,因为这里存在意外重叠的风险。不过,我的存档读取代码在我第一次尝试时就工作了,这对我来说足以证明这段代码足以完成手头的任务。
我不得不解决这个问题一次。数据文件打包为FORTRAN输出。对齐都是错误的。我成功地使用预处理器技巧,这些技巧会自动执行您手动执行的操作:将原始数据从字节缓冲区解压缩到结构体。这个想法是在包含文件中描述数据:
BEGIN_STRUCT(foo)
UNSIGNED_SHORT(length)
STRING_FIELD(length, label)
UNSIGNED_INT(stride)
FLOAT_ARRAY(3 * stride)
END_STRUCT(foo)
现在你可以定义这些宏来生成你需要的代码,比如结构声明,包括上面的,undef 并再次定义宏以生成解包函数,然后是另一个包含,等等。
注意:我第一次在 gcc 中看到这种技术用于抽象语法树相关的代码生成。
如果 CPP 不够强大(或者这种预处理器滥用不适合您(,请替换一个小的 lex/yacc 程序(或选择您最喜欢的工具(。
令我惊讶的是,至少在像这样的低级基础代码中,从生成代码而不是手动编写代码的角度来思考是值得的。
你应该更好地声明一个结构(带有 1 字节填充 - 如何 - 取决于编译器(。使用该结构写入,并使用相同的结构读取。只将 POD 放入结构中,因此没有std::string
等。此结构仅用于文件 I/O 或其他进程间通信 - 使用普通struct
或class
来保存它,以便在程序中进一步使用C++。
由于所有数据都是可变的,因此您可以分别读取这两个块并仍然使用强制转换:
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.
我个人是这样做的:
// 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;
在文件开头固定大小的结构非常有效的方法。
使用序列化库。以下是一些:
- 提升
- 序列化和提升融合
- 麦片(我自己的图书馆(
- 另一个叫做谷物的图书馆(与我的同名,但我的早于他们的(
- 普罗托角
Kaitai Struct 库提供了一种非常有效的声明性方法,该方法具有跨编程语言工作的额外好处。
安装编译器后,您需要创建一个描述二进制文件布局的.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 结构编译器ksc
编译.ksy
文件:
# 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 的完整文档。它具有大量其他功能,并且是一般二进制解析的绝佳资源。
我使用ragel
工具为具有1-2KRAM的微控制器生成纯C程序源代码(无表(。它没有使用任何文件io,缓冲,并生成易于调试的代码和带有状态机图的.dot/.pdf文件。
ragel还可以输出go、Java,..代码进行解析,但我没有使用这些功能。
ragel
的关键功能是能够解析任何字节构建数据,但您无法深入研究位字段。另一个问题是 ragel 能够解析常规结构,但没有递归和语法语法解析。
- 使用QQuickFramebufferObject时同步数据的最佳方式是什么
- 在reactor中存储eventHandlers的最佳方式是什么
- 引用 std::any 或 not_yet_in_std::whatever 的惯用方式是什么?
- 在C++中,建议通过数组循环的方式是什么?
- 哪种方式更快?究竟发生了什么,我们没有看到什么?
- DLL共享数据的推荐方式是什么
- 等待线程的最佳方式是什么
- 将uint8_t*buffer和size_tbufferlen从C++传递到C中的API函数的最佳方式是什么
- 只显示片段着色器的最佳方式是什么
- 复制文件的最佳方式是什么,以便我可以在复制过程中轻松取消复制?
- 在 c++ 中打印到控制台的最佳方式是什么?
- 在Qt Creator中应用代码更改的快捷方式是什么?
- 尝试使用 Qt 库中的 QPixmap 将图像拆分为多个块。关于他的复制方法的工作方式,我有什么不明白的吗?
- 在C 中超负荷构造函数的合适方式是什么
- 这个正则表达式的括号以什么方式"mismatched"?
- 打开文件的正确模式是什么,以便 seekp() 的工作方式与在默认模式下打开的文件相同
- 我可以使用什么方法或方式将用户输入的字符串转换为 C++ 中的整数
- 有什么优雅的方式吗?(类型参数包)
- 执行随机开关函数的QT方式是什么连续两次使用相同情况的方法
- 在某些代码中覆盖方法的方式是什么?