19

我有一个在 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 上的 little-endian 和 unix 环境中的 big-endian 中,我明白了。对于两个字节,Windows 上的 0x1234 将是 unix 系统上的 0x3412。

字节序会影响整个结构的字节顺序,还是影响结构中每个单独成员的字节顺序?我将采取什么方法将在 unix 系统上创建的结构转换为在 Windows 系统上具有相同数据的结构?任何比几个字节的字节顺序更深入的链接也会很棒!

4

8 回答 8

12

除了字节序,您还需要注意两个平台之间的填充差异。特别是如果您有奇数长度的 char 数组和 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);
于 2009-05-13T18:27:52.693 回答
10

实际上,字节顺序是底层硬件的属性,而不是操作系统的属性。

最好的解决方案是在写入数据时转换为标准——谷歌的“网络字节顺序”,你应该找到这样做的方法。

编辑:这是链接:http ://www.gnu.org/software/hello/manual/libc/Byte-Order.html

于 2009-05-13T18:22:20.457 回答
8

不要直接从文件中读入结构!打包可能会有所不同,您必须摆弄 pragma pack 或类似的编译器特定结构。太不靠谱了。许多程序员侥幸逃脱,因为他们的代码没有在大量架构和系统中编译,但这并不意味着可以这样做!

一个很好的替代方法是将标头读取到缓冲区中并从三个中解析,以避免原子操作中的 I/O 开销,例如读取无符号 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();

阅读器对象显然只是指针周围的薄包装。size 参数将用于错误检查(如果有)。对于接口本身来说并不是强制性的。

注意这里的字节序选择是如何在编译时完成的(因为我们创建了 little_endian_reader 对象),所以我们调用虚方法开销没有特别好的理由,所以我不会采用这种方法。;-)

在这个阶段,没有真正的理由保持“文件格式结构”保持原样,您可以根据自己的喜好组织数据,而不必将其读入任何特定的结构;毕竟,这只是数据。当您读取图像之类的文件时,您实际上并不需要标题。您应该拥有对所有文件类型都相同的图像容器,因此读取特定格式的代码应该只读取文件,解释并重新格式化数据并存储有效载荷。=)

我的意思是,这看起来很复杂吗?

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

代码看起来不错,而且开销非常低!如果编译代码的文件和体系结构的字节序相同,则内部循环可能如下所示:

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

这在某些架构上可能是非法的,因此优化可能是一个坏主意,并使用更慢但更健壮的方法:

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

在可以编译为 bswap 或 mov 的 x86 上,如果方法是内联的,则开销相当低;编译器会将“移动”节点插入中间代码,仅此而已,这是相当有效的。如果对齐是一个问题,则可能会生成完整的读取移位或序列,但仍然不会太破旧。如果测试地址 LSB 并查看是否可以使用快速或慢速版本的解析,比较分支可以允许优化。但这意味着每次读取都会受到测试的惩罚。可能不值得努力。

哦,对了,我们正在阅读 HEADERS 之类的东西,我认为这不是太多应用程序的瓶颈。如果某些编解码器正在执行一些非常紧密的内部循环,那么再次读取临时缓冲区并从那里解码是明智的。相同的原理.. 在处理大量数据时,没有人从文件中读取一个字节。好吧,实际上,我经常看到这种代码,通常对“你为什么这样做”的回答是文件系统会阻塞读取,并且字节无论如何都来自内存,没错,但它们经过了一个很深的调用堆栈这是获取几个字节的高开销!

尽管如此,编写一次解析器代码并使用无数次 -> 史诗般的胜利。

从文件中直接读取结构:不要这样做!

于 2009-06-29T13:38:11.763 回答
4

它独立影响每个成员,而不是整体struct。此外,它不会影响数组之类的东西。例如,它只是使ints 中的字节以相反的顺序存储。

PS。也就是说,可能有一台机器具有奇怪的字节序。我刚才所说的适用于大多数使用的机器(x86、ARM、PowerPC、SPARC)。

于 2009-05-13T18:21:53.420 回答
1

您必须单独更正超过一个字节的每个成员的字节序。字符串不需要转换(fooword 和 barword),因为它们可以被视为字节序列。

但是,您必须注意另一个问题:结构中成员的对齐。基本上,您必须检查 unix 和 windows 代码上的 sizeof(RECORD) 是否相同。编译器通常提供编译指示来定义您想要的对齐方式(例如,#pragma pack)。

于 2009-05-13T18:27:45.843 回答
1

您还必须考虑两个编译器之间的对齐差异。每个编译器都可以在最适合架构的结构中的成员之间插入填充。所以你真的需要知道:

  • UNIX prog 如何写入文件
  • 如果它是对象的二进制副本,则结构的确切布局。
  • 如果它是二进制副本,源架构的字节序是什么。

这就是为什么大多数程序(我见过的(需要与平台无关的))将数据序列化为标准 iostream 可以轻松读取的文本流的原因。

于 2009-05-13T18:31:42.267 回答
1

我喜欢为需要交换的每种数据类型实现一个 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 重载都有一个特定于平台的实现。

于 2009-05-13T19:02:01.607 回答
0

像这样的东西应该工作:

#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;
}
于 2009-05-13T18:35:07.267 回答