Чтение двоичного файла, определенного структурой

Может ли кто-нибудь указать мне правильное направление, как я мог бы прочитать двоичный файл, который определяется структурой 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. Оригинальная структура упаковки / выравнивания также может быть важна.

Другие вопросы по тегам