如何将big-endian结构转换为小端结构?

时间:2009-05-13 18:19:47

标签: c++ struct endianness

我有一个在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。

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

8 个答案:

答案 0 :(得分:13)

与endian一样,您需要了解两个平台之间的填充差异。特别是如果您有奇数长度的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);

答案 1 :(得分:10)

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

最好的解决方案是在编写数据时转换为标准 - Google用于“网络字节顺序”,您应该找到执行此操作的方法。

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

答案 2 :(得分:5)

不要直接从文件中读取结构!打包可能不同,你必须摆弄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..

很容易切换相同代码的endianess,但很少需要的功能..文件格式通常都有固定的endianess。

请勿使用虚拟方法将其抽象为类;只会增加开销,但如果愿意,请随意:

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

读者对象显然只是指针周围的薄包装。 size参数用于错误检查(如果有)。对于接口本身并不是必须的。

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

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

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

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和东西,我不认为这是太多应用程序的瓶颈。如果某些编解码器正在做一些非常紧密的内环,再次,读入一个临时缓冲区并从那里进行解码是很好的建议。同样的原则..在处理大量数据时,没有人从文件中按字节读取。好吧,实际上,我经常看到那种代码并且通常回复“你为什么这样做”是文件系统阻止读取并且字节来自内存无论如何,是真的,但它们通过深度调用堆栈这是获得几个字节的高开销!

仍然,编写一次解析器代码并使用数万次 - &gt;史诗般的胜利。

直接从文件中读取结构:不要做任何事情!

答案 3 :(得分:3)

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

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

答案 4 :(得分:1)

您必须单独更正多个字节的每个成员的字节顺序。字符串不需要转换(fooword和barword),因为它们可以看作是字节序列。

但是,您必须处理另一个问题:结构中成员的对应关系。基本上,您必须检查unix和windows代码上的sizeof(RECORD)是否相同。编译器通常提供编译指示来定义所需的对象(例如,#pragma pack)。

答案 5 :(得分:1)

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

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

这就是为什么大多数程序(我已经看到(需要平台中立))将数据序列化为文本流,可以通过标准的iostream轻松读取。

答案 6 :(得分: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重载的特定于平台的实现。

答案 7 :(得分: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;
}