Чтение двоичного файла, определенного структурой
Может ли кто-нибудь указать мне правильное направление, как я мог бы прочитать двоичный файл, который определяется структурой 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]
}
Я был бы очень признателен, если бы кто-нибудь указал мне правильное направление, как это сделать. Или, если есть какой-то учебник, который охватывает эту тему?
Заранее большое спасибо за вашу помощь.
5 ответов
Чтение двоичного файла, определенного структурой, очень просто.
Format myFormat;
fread(&myFormat, sizeof(Format), 1, fp);
#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 является либо неоперативным, либо байтовым реверсером в зависимости от платформы.
Есть некоторые плохие идеи и хорошие идеи:
Это плохая идея для:
- Введите тип необработанного буфера в структуру
- Существуют проблемы с порядком байтов (little-endian против big-endian) при разборе целых чисел>1 байт или чисел с плавающей точкой
- Есть проблемы с выравниванием байтов в структурах, которые очень зависят от компилятора. Можно попытаться отключить выравнивание (или принудительно настроить выравнивание вручную), но в целом это тоже плохая идея. По крайней мере, вы снизите производительность, сделав доступ к процессору невыровненными целыми числами. Внутреннее ядро RISC должно было бы выполнить 3-4 операции вместо 1 (то есть "выполнить часть 1 в первом слове", "выполнить часть 2 во втором слове", "объединить результат"), чтобы получить к нему доступ каждый раз. Или, что еще хуже, прагмы компилятора, управляющие выравниванием, будут игнорироваться, и ваш код сломается.
- Там нет точных размеров гарантии для регулярного
int
,long
,short
и т.д., введите C/C++. Вы можете использовать такие вещи, какint16_t
, но они доступны только на современных компиляторах. - Конечно, этот подход полностью нарушается при использовании структур, которые ссылаются на другие структуры: нужно развернуть их все вручную.
- Писать парсеры вручную: это гораздо сложнее, чем кажется на первый взгляд.
- Хороший парсер должен много проверять работоспособность на каждом этапе. Легко что-то пропустить. Еще проще что-то пропустить, если вы не используете исключения.
- Использование исключений делает вас склонным к сбою, если ваш код синтаксического анализа не является безопасным для исключений (т. Е. Записан таким образом, что он может быть прерван в некоторых точках и не пропустит память / не забудет завершить некоторые объекты)
- Могут быть проблемы с производительностью (то есть выполнение большого количества небуферизованного ввода-вывода вместо одной ОС
read
системный вызов и синтаксический анализ буфера, а затем - или наоборот, чтение всего объекта сразу, а не более детальное, ленивое чтение, где это применимо).
Это хорошая идея
- Перейти кросс-платформенный. В значительной степени говорят сами за себя, со всеми мобильными устройствами, маршрутизаторами и вещами IoT, процветающими в последние годы.
- Иди декларативно. Рассмотрите возможность использования любой декларативной спецификации для описания вашей структуры, а затем используйте генератор синтаксического анализатора для генерации синтаксического анализатора.
Для этого есть несколько инструментов:
- Kaitai Struct - мой любимый до сих пор кросс-платформенный кросс-язык - то есть вы описываете свою структуру один раз, а затем можете скомпилировать ее в синтаксический анализатор на C++, C#, Java, Python, Ruby, PHP и т. Д.
- binpac - довольно устаревший, но все еще применимый, только на C++- похож на Kaitai по идеологии, но не поддерживается с 2013 года
- Spicy - называется "современное переписывание" binpac, AKA "binpaC++", но все еще на ранних стадиях разработки; может использоваться для небольших задач, только C++.
Использование библиотеки ввода-вывода C++:
#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);
Вы также можете использовать союзы для этого анализа, если у вас есть данные, которые вы хотите проанализировать уже в памяти.
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 определенного размера, рассмотрите возможность использования типов, определенных int stdint.h, таких как uint32_t.
Вы должны выяснить порядковые номера машины, на которой был записан файл, чтобы вы могли правильно интерпретировать целые числа. Обратите внимание на несоответствие ILP32 и LP64. Оригинальная структура упаковки / выравнивания также может быть важна.