Как мне преобразовать структуру с прямым порядком байтов в небольшую структуру с прямым порядком байтов? - PullRequest
18 голосов
/ 13 мая 2009

У меня есть двоичный файл, который был создан на Unix-машине. Это просто куча записей, написанных одна за другой. Запись определяется примерно так:

struct RECORD {
  UINT32 foo;
  UINT32 bar;
  CHAR fooword[11];
  CHAR barword[11];
  UNIT16 baz;
}

Я пытаюсь выяснить, как я буду читать и интерпретировать эти данные на компьютере с Windows. У меня есть что-то вроде этого:

fstream f;
f.open("file.bin", ios::in | ios::binary);

RECORD r;

f.read((char*)&detail, sizeof(RECORD));

cout << "fooword = " << r.fooword << endl;

Я получаю кучу данных, но это не те данные, которые я ожидаю. Я подозреваю, что моя проблема связана с разницей порядков байтов машин, поэтому я пришел спросить об этом.

Я понимаю, что несколько байтов будут храниться с прямым порядком байтов в Windows и с прямым порядком байтов в среде Unix, и я понял. Для двух байтов 0x1234 в Windows будет 0x3412 в Unix-системе.

Влияет ли порядок байтов на порядок байтов структуры в целом или каждого отдельного члена структуры? Какие подходы я бы использовал для преобразования структуры, созданной в системе Unix, в структуру, имеющую те же данные в системе Windows? Любые ссылки, которые глубже, чем порядок байтов пары байтов, тоже были бы хорошими!

Ответы [ 8 ]

13 голосов
/ 13 мая 2009

Как и в случае с порядком байтов, вы должны знать о различиях между отступами между двумя платформами. В частности, если у вас есть массивы символов нечетной длины и 16-битные значения, вы можете найти различное количество байтов пэдов между некоторыми элементами.

Редактировать: если структура была записана без упаковки, то она должна быть достаточно простой. Что-то вроде этого (непроверенного) кода должно делать эту работу:

// Functions to swap the endian of 16 and 32 bit values

inline void SwapEndian(UINT16 &val)
{
    val = (val<<8) | (val>>8);
}

inline void SwapEndian(UINT32 &val)
{
    val = (val<<24) | ((val<<8) & 0x00ff0000) |
          ((val>>8) & 0x0000ff00) | (val>>24);
}

Затем, как только вы загрузите структуру, просто поменяйте местами каждый элемент:

SwapEndian(r.foo);
SwapEndian(r.bar);
SwapEndian(r.baz);
10 голосов
/ 13 мая 2009

На самом деле, endianness - это свойство базового оборудования, а не ОС.

Лучшее решение заключается в преобразовании в стандарт при записи данных - Google для "сетевого порядка байтов", и вы должны найти методы для этого.

Редактировать: вот ссылка: http://www.gnu.org/software/hello/manual/libc/Byte-Order.html

5 голосов
/ 29 июня 2009

Не читайте напрямую в структуру из файла! Упаковка может быть другой, вы должны возиться с пакетом pragma или аналогичными конструкциями, специфичными для компилятора. Слишком ненадежно. Многим программистам это сходит с рук, поскольку их код не скомпилирован в большом количестве архитектур и систем, но это не значит, что это нормально!

Хороший альтернативный подход - это прочитать заголовок, в любом случае, в буфер и проанализировать три, чтобы избежать издержек ввода-вывода в элементарных операциях, таких как чтение 32-разрядного целого числа без знака!

char buffer[32];
char* temp = buffer;  

f.read(buffer, 32);  

RECORD rec;
rec.foo = parse_uint32(temp); temp += 4;
rec.bar = parse_uint32(temp); temp += 4;
memcpy(&rec.fooword, temp, 11); temp += 11;
memcpy(%red.barword, temp, 11); temp += 11;
rec.baz = parse_uint16(temp); temp += 2;

Объявление parse_uint32 будет выглядеть так:

uint32 parse_uint32(char* buffer)
{
  uint32 x;
  // ...
  return x;
}

Это очень простая абстракция, на практике обновление указателя также не требует дополнительных затрат:

uint32 parse_uint32(char*& buffer)
{
  uint32 x;
  // ...
  buffer += 4;
  return x;
}

Более поздняя форма позволяет более чистый код для анализа буфера; указатель автоматически обновляется при разборе входных данных.

Аналогично, у memcpy может быть помощник, что-то вроде:

void parse_copy(void* dest, char*& buffer, size_t size)
{
  memcpy(dest, buffer, size);
  buffer += size;
}

Прелесть такого рода организации заключается в том, что у вас могут быть пространства имен "little_endian" и "big_endian", тогда вы можете сделать это в своем коде:

using little_endian;
// do your parsing for little_endian input stream here..

Легко переключать порядковый номер для одного и того же кода, однако, редко необходимая функция. В любом случае форматы файлов обычно имеют фиксированный порядковый номер.

НЕ абстрагируйте это в класс с помощью виртуальных методов; просто добавит накладные расходы, но не стесняйтесь, если так склонны:

little_endian_reader reader(data, size);
uint32 x = reader.read_uint32();
uint32 y = reader.read_uint32();

Объект читателя, очевидно, будет просто тонкой оболочкой вокруг указателя. Параметр размера будет для проверки ошибок, если таковые имеются. Не совсем обязательно для интерфейса как такового.

Обратите внимание, как выбор порядка байтов здесь был сделан во время компиляции (так как мы создаем объект little_endian_reader), поэтому мы вызываем издержки виртуального метода без особой веской причины, поэтому я бы не стал использовать этот подход. ; -)

На этом этапе нет реальной причины сохранять «структуру файлового формата» как есть, вы можете организовать данные по своему вкусу и вовсе не обязательно читать их в какой-либо конкретной структуре; в конце концов, это просто данные. Когда вы читаете файлы, например изображения, вам не нужен заголовок вокруг ... у вас должен быть контейнер изображений, который одинаков для всех типов файлов, поэтому код для чтения определенного формата должен просто читать файл, интерпретировать и переформатировать данные и хранить полезную нагрузку. =) * * 1 029

Я имею в виду, это выглядит сложно?

uint32 xsize = buffer.read<uint32>();
uint32 ysize = buffer.read<uint32>();
float aspect = buffer.read<float>();    

Код может выглядеть так красиво и очень экономно! Если порядок байтов одинаков для файла и архитектуры, для которой компилируется код, innerloop может выглядеть следующим образом:

uint32 value = *reinterpret_cast<uint32*>)(ptr); ptr += 4;
return value;

Это может быть недопустимо на некоторых архитектурах, так что оптимизация может быть плохой идеей и использовать более медленный, но более надежный подход:

uint32 value = ptr[0] | (static_cast<uint32>(ptr[1]) << 8) | ...; ptr += 4;
return value;

На x86, который может компилироваться в bswap или mov, что является довольно низким расходом, если метод встроен; компилятор вставит узел «move» в промежуточный код, ничего более, что довольно эффективно. Если выравнивание является проблемой, может быть сгенерирована полная последовательность чтения-смены или нет, но все же не слишком потертая. Ветвь сравнения может позволить оптимизацию, если протестировать адрес LSB и посмотреть, можно ли использовать быструю или медленную версию синтаксического анализа. Но это будет означать штраф за тест в каждом чтении. Не стоило бы усилий.

О, верно, мы читаем заголовки и прочее, я не думаю, что это является узким местом во многих приложениях. Если какой-то кодек выполняет действительно НАСТОЯЩУЮ внутреннюю петлю, опять же рекомендуется чтение во временный буфер и декодирование оттуда. Тот же принцип .. никто не читает по байтам из файла при обработке большого объема данных. Ну, на самом деле, я видел такой код очень часто, и обычный ответ на вопрос «почему вы это делаете» заключается в том, что файловые системы выполняют блокировку чтения и что байты все равно приходят из памяти, правда, но они проходят через глубокий стек вызовов что требует много байтов!

Тем не менее, напишите код парсера один раз и используйте миллионы раз -> эпический выигрыш.

Чтение непосредственно в struct из файла: НЕ ДЕЛАЙТЕ НАРОДОВ!

3 голосов
/ 13 мая 2009

Влияет на каждого члена независимо, а не на все struct. Кроме того, это не влияет на такие вещи, как массивы. Например, он просто создает байты в int с, сохраненные в обратном порядке.

PS. Тем не менее, может быть машина со странным порядком байтов. То, что я только что сказал, относится к большинству используемых машин (x86, ARM, PowerPC, SPARC).

1 голос
/ 13 мая 2009

Мне нравится реализовывать метод SwapBytes для каждого типа данных, который требует замены, например:

inline u_int ByteSwap(u_int in)
{
    u_int out;
    char *indata = (char *)&in;
    char *outdata = (char *)&out;
    outdata[0] = indata[3] ;
    outdata[3] = indata[0] ;

    outdata[1] = indata[2] ;
    outdata[2] = indata[1] ;
    return out;
}

inline u_short ByteSwap(u_short in)
{
    u_short out;
    char *indata = (char *)&in;
    char *outdata = (char *)&out;
    outdata[0] = indata[1] ;
    outdata[1] = indata[0] ;
    return out;
}

Затем я добавляю функцию в структуру, которая требует замены, например:

struct RECORD {
  UINT32 foo;
  UINT32 bar;
  CHAR fooword[11];
  CHAR barword[11];
  UNIT16 baz;
  void SwapBytes()
  {
    foo = ByteSwap(foo);
    bar = ByteSwap(bar);
    baz = ByteSwap(baz);
  }
}

Затем вы можете изменить свой код, который читает (или записывает) структуру следующим образом:

fstream f;
f.open("file.bin", ios::in | ios::binary);

RECORD r;

f.read((char*)&detail, sizeof(RECORD));
r.SwapBytes();

cout << "fooword = " << r.fooword << endl;

Для поддержки различных платформ вам просто необходимо иметь реализацию каждой перегрузки ByteSwap для конкретной платформы.

1 голос
/ 13 мая 2009

Вы также должны учитывать различия в выравнивании между двумя компиляторами. Каждому компилятору разрешено вставлять отступы между членами в структуре, которая наилучшим образом соответствует архитектуре. Так что вам действительно нужно знать:

  • Как программа UNIX записывает в файл
  • Если это двоичная копия объекта, точная схема структуры.
  • Если это бинарная копия, что означает порядковый номер исходной архитектуры.

Именно поэтому большинство программ (которые я видел (которые должны быть независимыми от платформы)) сериализуют данные в виде текстового потока, который легко читается стандартными iostreams.

1 голос
/ 13 мая 2009

Вы должны исправить порядковый номер каждого члена более чем в одном байте в отдельности. Строки не нужно преобразовывать (fooword и barword), так как они могут рассматриваться как последовательности байтов.

Однако вы должны позаботиться о другой проблеме: принадлежности членов вашей структуры. По сути, вы должны проверить, одинаков ли sizeof (RECORD) в коде Unix и Windows. Компиляторы обычно предоставляют прагмы для определения требуемой надписи (например, #pragma pack).

0 голосов
/ 13 мая 2009

Примерно так должно работать:

#include <algorithm>

struct RECORD {
    UINT32 foo;
    UINT32 bar;
    CHAR fooword[11];
    CHAR barword[11];
    UINT16 baz;
}

void ReverseBytes( void *start, int size )
{
    char *beg = start;
    char *end = beg + size;

    std::reverse( beg, end );
}

int main() {
    fstream f;
    f.open( "file.bin", ios::in | ios::binary );

    // for each entry {
    RECORD r;
    f.read( (char *)&r, sizeof( RECORD ) );
    ReverseBytes( r.foo, sizeof( UINT32 ) );
    ReverseBytes( r.bar, sizeof( UINT32 ) );
    ReverseBytes( r.baz, sizeof( UINT16 )
    // }

    return 0;
}
...