如何编写endian不可知的C / C ++代码?

时间:2012-12-21 17:36:56

标签: c++ c endianness

我做了一些谷歌搜索,在这个问题上找不到任何好文章。在实现我想要与endian无关的应用时,我应该注意什么?

7 个答案:

答案 0 :(得分:28)

您唯一需要关心字节序的时间是在可能不具有相同字节序的系统之间传输字节敏感的二进制数据(即非文本)。通常的解决方案是使用“network byte order”(AKA big-endian)来传输数据,然后在必要时在另一端调用字节。

要从主机转换为网络字节顺序,请使用htons(3)htonl(3)。要转换回来,请使用ntohl(3)ntohs(3)。查看man page了解您需要了解的所有信息。对于64位数据,this question and answer会有所帮助。

答案 1 :(得分:17)

  

在实现我希望与endian无关的应用时,我应该注意什么?

首先必须认识到endian成为问题。当你必须从外部的某个地方读取或写入数据时,无论是从文件读取数据还是在计算机之间进行网络通信,它都会成为一个问题。

在这种情况下,endianess对于大于一个字节的整数很重要,因为整数在内存中由不同的平台表示不同。这意味着每次需要读取或写入外部数据时,您需要做的不仅仅是转储程序的内存,或者直接将数据读入您自己的变量。

e.g。如果你有这段代码:

unsigned int var = ...;
write(fd, &var, sizeof var);

您直接写出var的内存内容,这意味着数据会显示在此数据所在的位置,就像在您自己的计算机内存中一样。

如果将此数据写入文件,无论是在大端程序还是小端程序机器上运行程序,文件内容都会有所不同。所以代码不是endian不可知的,你要避免做这样的事情。

而是专注于数据格式。在读/写数据时,始终先确定数据格式,然后编写代码来处理它。如果您需要阅读一些现有的明确定义的文件格式或实现现有的网络协议,这可能已经为您决定。

一旦你知道数据格式,而不是例如直接转储一个int变量,你的代码执行此操作:

uint32_t i = ...;
uint8_t buf[4];
buf[0] = (i&0xff000000) >> 24;
buf[1] = (i&0x00ff0000) >> 16;
buf[2] = (i&0x0000ff00) >> 8;
buf[3] = (i&0x000000ff);
write(fd, buf, sizeof buf);

我们现在选择了最重要的字节,并将其作为缓冲区中的第一个字节,并将最低有效字节放在缓冲区的末尾。该整数在buf中以大端格式表示,与主机的端序无关 - 因此此代码与字节序无关。

此数据的使用者必须知道数据以大端格式表示。无论程序运行的主机如何,这段代码都可以很好地读取数据:

uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i  = (uint32_t)buf[0] << 24;
i |= (uint32_t)buf[1] << 16;
i |= (uint32_t)buf[2] << 8;
i |= (uint32_t)buf[3];

相反,如果您需要读取的数据是小端格式,那么endianess不可知代码就可以了

uint32_t i ;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i  = (uint32_t)buf[3] << 24;
i |= (uint32_t)buf[2] << 16;
i |= (uint32_t)buf[1] << 8;
i |= (uint32_t)buf[0];

你可以制作一些漂亮的内联函数或宏来包装和解包你需要的所有2,4,8字节整数类型,如果你使用它们并关心数据格式而不是你运行的处理器的字节序,您的代码将不依赖于它正在运行的结束。

这是比许多其他解决方案更多的代码,我还没有编写一个程序,其中这项额外的工作对性能产生了任何有意义的影响,即使在对1Gbps以上的数据进行混乱时也是如此。

它还避免了错误的内存访问,您可以通过例如

的方法轻松获得
uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i = ntohl(*(uint32_t)buf));

这也可能导致性能损失(在某些情况下微不足道,在其他情况下有很多数量级),在无法对整数进行未对齐访问的平台上崩溃更糟糕。

答案 2 :(得分:13)

这可能是一篇很好的文章供您阅读:The byte order fallacy

  

除了编译器编写器之外,计算机的字节顺序并不重要,编译器编写器等会对映射到寄存器片段的内存字节分配进行大惊小怪。您可能不是编译器编写者,因此计算机的字节顺序对您来说无关紧要。

     

注意短语“计算机的字节顺序”。重要的是外围设备或编码数据流的字节顺序,但是 - 这是关键点 - 执行处理的计算机的字节顺序与数据本身的处理无关。如果数据流用字节顺序B编码值,那么用字节顺序C解码计算机上的值的算法应该是B,而不是B和C之间的关系。

答案 3 :(得分:8)

有几个答案涵盖了文件IO,这肯定是最常见的endian问题。我会谈到一个尚未提及的人:工会

以下联合是SIMD / SSE编程中的常用工具,并且对字节顺序友好:

union uint128_t {
    _m128i      dq;
    uint64_t    dd[2];
    uint32_t    dw[4];
    uint16_t    dh[8];
    uint8_t     db[16];
};

访问dd / dw / dh / db表单的任何代码都将以特定于endian的方式执行。在32位CPU上,看到更简单的联合更容易将64位算术分解为32位部分也是常见的:

union u64_parts {
    uint64_t    dd;
    uint32_t    dw[2];
};

因为在这个用例中很少(如果有的话)想要遍历union的每个元素,我更喜欢写这样的联合:

union u64_parts {
    uint64_t dd;
    struct {
#ifdef BIG_ENDIAN
        uint32_t dw2, dw1;
#else
        uint32_t dw1, dw2;
#endif
    }
};

结果是对任何直接访问dw1 / dw2的代码进行隐式endian-swapping。相同的设计方法也可用于上面的128位SIMD数据类型,但最终会更加冗长。

免责声明:由于关于结构填充和对齐的标准定义松散,因此联盟的使用常常令人不悦。我发现工会非常有用并且已经广泛使用它们,并且我在很长一段时间(15年以上)都没有遇到任何交叉兼容性问题。对于任何针对x86,ARM或PowerPC的当前编译器,Union padding / alignment将以预期和一致的方式运行。

答案 4 :(得分:2)

在你的代码中你几乎可以忽略它 - 一切都取消了。

当您将数据读/写到磁盘或网络时使用htons

答案 5 :(得分:1)

这显然是一个颇具争议的话题。

一般的方法是设计你的应用程序,使你只关心一小部分的字节顺序:代码的输入和输出部分。

在其他地方,您应该使用本机字节顺序。

请注意,尽管MOST机器以相同的方式执行此操作,但不能保证浮点数和整数数据以相同的方式存储,因此要确保事情正常,您不仅需要知道大小,还需要知道无论是整数还是浮点数。

另一种选择是仅以文本格式使用和生成数据。这可能几乎同样容易实现,除非您在很少处理的情况下在应用程序中输入/输出非常高的数据率,否则性能差异可能非常小。并且有利于(对某些人)您可以在文本编辑器中读取输入和输出数据,而不是尝试解码输出中实际应该是字节51213498-51213501的值,当您遇到错误时代码。

答案 6 :(得分:0)

如果需要在2,4或8字节整数类型和字节索引数组之间重新解释(反之亦然),则需要知道字节序。

这经常出现在加密算法实现,序列化应用程序(如网络协议,文件系统或数据库后端),当然还有操作系统内核和驱动程序中。

它通常由像ENDIAN ......这样的宏检测到。

例如:

uint32 x = ...;
uint8* p = (uint8*) &x;

p指向BE机器上的高字节,以及LE机器上的低字节。

使用您可以编写的宏:

uint32 x = ...;

#ifdef LITTLE_ENDIAN
    uint8* p = (uint8*) &x + 3;
#else // BIG_ENDIAN
    uint8* p = (uint8*) &x;
#endif

总是得到高字节,例如。

如果你的工具链没有提供宏,可以在这里定义宏:C Macro definition to determine big endian or little endian machine?