如何将大端结构体转换为小端结构体?

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上,多个字节将以小端字节序存储,在Unix环境中则为大端字节序,我理解这点。对于两个字节,Windows上的0x1234在Unix系统上将是0x3412。

字节序是否影响结构体作为整体的字节顺序,还是每个结构体成员的字节顺序?我应该采取什么方法将在Unix系统上创建的结构体转换为在Windows系统上具有相同数据的结构体?如果有比几个字节的字节顺序更深入的链接,那就太好了!


1
你没有问到它们,但在处理这种遗留代码时需要考虑的另一件事是位域。位域的打包顺序可能取决于编译器和平台,并且与处理器的字节顺序无关。 - Dan
8个回答

13

除了字节序,你还需要注意两个平台之间的填充差异。特别是如果你有奇数长度的字符数组和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);

我已经指定了 #pragma pack(push,1)。 - scottm
1
@Scotty,如果你读取的数据已经有松散字节在其中,那么这并不能帮助你。顺便说一下,除非程序开发人员写出了完整的结构体,否则这种情况不应该发生,这是很糟糕的。结构体应该始终按字段逐个写出,以应对这种情况。 - Duck
@Duck,我有结构定义的源代码(但不能用于读取或写入),并且它还具有pack = 1。 - scottm
1
如果元素只包含一个值,这个方法可以工作。请记得,如果该元素是一个数组,要写一个循环来交换所有值的字节序。 - HongboZhu

10

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

最好的解决方案是在写入数据时转换为标准格式——搜索“网络字节顺序”,您应该会找到做此操作的方法。

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


2
我不能决定如何编写数据,这个过程已经存在了10年,而且不会改变。 - scottm
1
在某些情况下,您需要发现所使用的确切机制,并编写自己的例程来转换(或在线查找)。但请注意,尽管编写者“不会改变”,但最好永远不要移动到另一种架构,否则它将无论喜欢与否都会改变。 - kdgregory

8

不要直接从文件中读取结构体!因为打包可能会有所不同,你需要使用pragma pack或类似的编译器特定结构进行调整。这太不可靠了。很多程序员之所以能够摆脱这个问题是因为他们的代码并没有在广泛的架构和系统中编译,但这并不意味着这样做是正确的!

一个好的替代方法是将头部等内容读入缓冲区,并从中解析,以避免像读取无符号32位整数这样的原子操作中的I/O开销!

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;

解析 uint32 的声明应该像这样:

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对象),因此我们出于没有特别好的原因调用了虚方法开销,所以我不会采用这种方法。;-)
在这个阶段,没有什么实际理由保留“文件格式结构”本身,您可以按照自己的喜好组织数据,甚至根本不需要将其读入任何特定的结构中;毕竟,它只是数据。当您读取像图像之类的文件时,您实际上不需要头信息..您应该有一个相同的图像容器,适用于所有文件类型,因此读取特定格式的代码应该只需读取文件,解释和重新格式化数据,并存储有效负载。=)
我的意思是,这看起来复杂吗?
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上,如果该方法被内联,则编译器会将“move”节点插入中间代码,除此之外不会有其他任何操作,这是相当高效的。如果对齐是个问题,则可能会生成完整的读取-移位-或序列,但仍然不算太差。比较分支可以允许优化,如果测试地址LSB并查看是否可以使用解析的快速或慢速版本。但这意味着在每次读取时都要进行测试的惩罚。也许不值得这样做。
哦,对了,我们正在读取头文件和其他东西,我认为这不是太多应用程序的瓶颈。如果某些编解码器正在执行一些非常紧密的内部循环,则再次建议将其读入临时缓冲区并从那里解码。同样的原则..处理大量数据时,没有人会从文件中逐字节读取。嗯,实际上,我经常看到那种代码,对于“为什么这样做”的通常回答是文件系统执行块读取,并且字节来自内存,这是正确的,但它通过深度调用堆栈,这对于获取几个字节而言开销很大!
仍然,编写一次解析器代码并使用无数次->史诗级胜利。
不要从文件中直接读取结构体:不要这样做,伙计们!

4

它独立影响每个成员,而不是整个struct。此外,它不会影响数组等其他东西。例如,它只会使存储在int中的字节顺序相反。

顺便提一句,也许有些机器具有奇怪的字节顺序。我刚才所说的适用于大多数使用的机器(x86、ARM、PowerPC、SPARC)。


但如果数组成员是数值数据类型或字符且大小大于1字节,则会受到影响!不过,它并不影响数组等其他事物。 - mmmmmmmm
1
@rstevens:是的,绝对没错。我的意思是它不会影响数组中元素的顺序。每个成员显然都像单个变量一样处理。 - Mehrdad Afshari

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重载的特定于平台的实现。

1
你必须分别更正多字节的每个成员的字节序。字符串(fooword和barword)不需要转换,因为它们可以被视为字节序列。
然而,你还必须注意另一个问题:结构体中成员的对齐方式。基本上,你必须检查在Unix和Windows代码中,sizeof(RECORD)是否相同。编译器通常提供pragma来定义所需的对齐方式(例如,#pragma pack)。

1

您还需要考虑两个编译器之间的对齐差异。每个编译器都允许在结构体成员之间插入填充以最适合架构的方式。因此,您确实需要知道:

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

这就是为什么大多数程序(我见过的需要平台中立的)将数据序列化为文本流,可以轻松地通过标准iostreams进行读取的原因。


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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接