是否有独立于体系结构的方法从 C 中的值创建小端字节流?

Is there an architecture-independent method to create a little-endian byte stream from a value in C?

我正在尝试通过创建 uint8_t[] 缓冲区然后发送它来在体系结构之间传输值。为确保它们被正确传输,规范是将所有值在进入缓冲区时转换为小端。

我阅读了这篇文章 here which discussed how to convert from one endianness to the other, and here,其中讨论了如何检查系统的字节顺序。

我很好奇是否有一种方法可以以小端顺序从 uint64 或其他值读取字节不管系统是大还是小? (即通过一些按位运算序列)

或者唯一的方法是首先检查系统的字节序,然后如果大显式转换为小?

您始终可以将 uint64_t 以小端顺序序列化为 uint8_t 的数组,如

uint64_t source = ...;
uint8_t target[8];

target[0] = source;
target[1] = source >> 8;
target[2] = source >> 16;
target[3] = source >> 24;
target[4] = source >> 32;
target[5] = source >> 40;
target[6] = source >> 48;
target[7] = source >> 56;

for (int i = 0; i < sizeof (uint64_t); i++) {
    target[i] = source >> i * 8;
}

这将适用于存在 uint64_tuint8_t 的任何地方。

请注意,这假设源值为 unsigned。位移负符号值会导致各种令人头疼的问题,而您只是不想这样做。


如果按顺序一次读取一个字节,反序列化会稍微复杂一些:

uint8_t source[8] = ...;
uint64_t target = 0;

for (int i = 0; i < sizeof (uint64_t); i ++) {
    target |= (uint64_t)source[i] << i * 8;
}

转换为 (uint64_t) 是绝对必要的,因为 << 的操作数将进行整数提升,而 uint8_t 将始终转换为 有符号 int - 当您将设置位移动到带符号 int.

的符号位时,会发生“有趣”的事情

如果你把它写成一个函数

#include <inttypes.h>

void serialize(uint64_t source, uint8_t *target) {
    target[0] = source;
    target[1] = source >> 8;
    target[2] = source >> 16;
    target[3] = source >> 24;
    target[4] = source >> 32;
    target[5] = source >> 40;
    target[6] = source >> 48;
    target[7] = source >> 56;
}

并使用 GCC 11 和 -O3 为 x86-64 编译,该函数将被编译为

serialize:
        movq    %rdi, (%rsi)
        ret

这只是将源的 64 位值按原样移动到目标数组中。如果你反转索引 (7 ... 0; big-endian),GCC 也会足够聪明地识别它并将它编译(使用 -O3)到

serialize:
        bswap   %rdi
        movq    %rdi, (%rsi)
        ret

这实际上很简单——您只需使用移位在 'native' 格式(无论是什么)和小端

之间进行转换
/* put a 32-bit value into a buffer in little-endian order (4 bytes) */
void put32(uint8_t *buf, uint32_t val) {
    buf[0] = val;
    buf[1] = val >> 8;
    buf[2] = val >> 16;
    buf[3] = val >> 24;
}

/* get a 32-bit value from a buffer (little-endian) */
uint32_t get32(uint8_t *buf) {
    return (uint32_t)buf[0] + ((uint32_t)buf[1] << 8) +
           ((uint32_t)buf[2] << 16) + ((uint32_t)buf[3] << 24);
}

如果你把一个值放入一个缓冲区,将它作为字节流传输到另一台机器,然后从接收缓冲区中获取值,两台机器无论是否有相同的32位值都会有相同或不同的本机字节顺序。需要强制转换,因为默认促销只会转换为 int,它可能小于 uin32_t,在这种情况下,转换可能超出范围。

如果您的缓冲区是 char 而不是 uint8_t 则要小心(字符可能已签名也可能未签名)——在这种情况下您需要屏蔽:

uint32_t get32(char *buf) {
    return ((uint32_t)buf[0] & 0xff) + (((uint32_t)buf[1] & 0xff) << 8) +
           (((uint32_t)buf[2] & 0xff) << 16) + (((uint32_t)buf[3] & 0xff) << 24);
}

大多数标准化网络协议都以大端格式指定数字。其实big-endian都是指network byte order,还有专门用来在host和network byte order之间转换各种大小整数的函数

这些函数对于 16 位值是 htonsntohs,对于 32 位值是 htonl 和 ntohl`。但是,没有 64 位值的等效项,并且您正在为网络协议使用小端,因此这些对您没有帮助。

但是,您仍然可以在不知道主机顺序的情况下在主机字节顺序和网络字节顺序(在本例中为小端)之间进行转换。您可以通过将相关值移入或移出主机编号来完成此操作。

例如,要将 32 位值从主机转换为小端并返回主机:

uint32_t src_value = *some value*;
uint8_t buf[sizeof(uint32_t)];
int i;

for (i=0; i<sizeof(uint32_t); i++) {
    buf[i] = (src_value >> (8 * i)) & 0xff;
}

uint32_t dest_value = 0;

for (i=0; i<sizeof(uint32_t); i++) {
    dest_value |= (uint32_t)buf[i] << (8 * i);
}

对于必须通信的两个系统,您指定“相互通信字节顺序”。然后你有在每个系统的本机架构字节顺序和本机体系结构字节顺序之间进行转换的函数。

这个问题有three approaches个。效率排序:

  1. 字节序的编译时检测
  2. 运行 字节顺序检测时间
  3. Endian 不可知代码(对应于您问题中的“按位操作序列”)。

字节序的编译时检测

在字节顺序与内部通信字节顺序相同的体系结构上,这些函数不进行任何转换,但通过使用它们,相同的代码可以在系统之间移植。

您的目标平台上可能已经存在此类功能,例如:

在它们不存在的地方创建具有跨平台支持的它们是微不足道的。例如:

uint16_t intercom_to_host_16( uint16_t intercom_word )
{
    #if __BIG_ENDIAN__
        return intercom_word ;
    #else
        return intercom_word >> 8 | intercom_word << 8 ;
    #endif
}

这里我假设内部通信顺序是大端,这使得该函数与 网络字节顺序 per ntohs() et al 兼容。宏 __BIG_ENDIAN__ 是大多数编译器上的预定义宏。如果不是在编译时简单地将其定义为命令行宏,例如-D __BIG_ENDIAN__.

运行 字节顺序检测时间

可以在运行时以最小的开销检测字节顺序:

uint16_t intercom_to_host_16( uint16_t intercom_word )
{
    static const union
    {
        uint16_t word ;
        uint8_t bytes[2] ;
    } test = {.word = 0xff00u } ;

    return test.bytes[0] == 0xffu ? 
                            intercom_word :
                            intercom_word >> 8 | intercom_word << 8 ;
}

当然,您可以将测试包装在一个函数中,以便在其他字长的类似函数中使用:

#include <stdbool.h>

bool isBigEndian()
{
        static const union
        {
            uint16_t word ;
            uint8_t bytes[2] ;
        } test = {.word = 0xff00u } ;

        return test.bytes[0] == 0xffu ;
}

然后只需:

uint16_t intercom_to_host_16( uint16_t intercom_word )
{
    return isBigEndian() ? intercom_word :
                           intercom_word >> 8 | intercom_word << 8 ;
}

Endian 不可知代码

完全可以使用端序不可知的代码,但在这种情况下,即使本机字节顺序已经与内部通信字节顺序相同,通信或文件处理中的所有参与者都会强加软件开销。

uint16_t intercom_to_host_16( uint16_t intercom_word )
{
    uint8_t host_word [2] = { intercom_word >> 8, 
                              intercom_word << 8 } ;
    return *(uint16_t*)host_word ;
}