如何从字节缓冲区读取 little-endian 64 位值?

How do I read a little-endian 64-bit value from a byte buffer?

在 C 应用程序(不是 C++)中,我有一个通过网络接收的数据字节数组。该数组长 9 个字节。字节 1 到 8(从零开始)表示一个 64 位整数值作为小端。我的CPU也用小端

如何将这些字节从数组转换为整数?

我试过这个:

uint8_t rx_buffer[2000];
//recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, ...)
int64_t sender_time_us = *(rx_buffer + 1);

但它给我的值是 89、219、234 或 27。发件人看到的值是 1647719702937548、1647719733002117 或 1647719743790424。(这些示例不匹配,它们只是随机样本。)

您的代码只得到一个 uint8_t。您需要先转换为 int64_t 。像这样:

int64_t* pBuffer = (int64_t*)(rx_buffer + 1);
int64_t sender_time_us = *pBuffer;

但您应该知道,某些 CPU 可能不喜欢访问未对齐的 64 位值。如果您知道字节序,也可以这样做,但实际上以更便携的方式处理它会更好。

你需要投射你的指针,像这样:

int64_t sender_time_us = *(int64_t*)(rx_buffer + 1);

实际上,您只会获得一个字节的数据。

不安全的解决方案:

int64_t sender_time_us = *(int64_t*)(rx_buffer + 1);

这可能是对齐违规,它是 strict aliasing rule violation. It's undefined behaviour. On some machines, this can kill your program with a bus error


安全解决方案:

int64_t sender_time_us;
memcpy( &sender_time_us, rx_buffer + 1, sizeof( int64_t ) );

@Nate Eldredge 指出,虽然这个解决方案可能看起来效率低下,但一个体面的编译器应该将其优化为高效的东西。最终效果将是 (a) 强制编译器正确处理未对齐的访问,如果目标需要任何特殊处理,(b) 让编译器正确理解别名并防止任何会破坏它的优化。对于能够正常处理未对齐访问的目标,生成的代码可能根本不会改变。

读取 little-endian 64 位值的可移植方式非常简单:

inline static uint64_t load_u64le(const void *p) {
    const unsigned char *q = p;
    uint64_t result = 0;
    result |= q[7]; result <<= 8;
    result |= q[6]; result <<= 8;
    result |= q[5]; result <<= 8;
    result |= q[4]; result <<= 8;
    result |= q[3]; result <<= 8;
    result |= q[2]; result <<= 8;
    result |= q[1]; result <<= 8;
    result |= q[0];
    return result;
}

inline static int64_t load_i64le(const void *p) {
    return (int64_t)load_u64le(p);
}

只需将此辅助函数调用为 read_i64le(rx_buffer + 1)。现代编译器能够 optimize this to a single instruction 在可能的架构上。

要读取您明确知道字节顺序与本机 ABI 一致的 64 位值,您可以使用:

inline static uint64_t load_u64(const void *p) {
    uint64_t result;
    memcpy(&result, p, sizeof(result));
    return result;
}

它有更好的机会被优化为简单加载,假设只假设编译器将短 memcpy 优化为内联内存加载。

为了获得最佳效果,您可以使用:

inline static uint64_t load_u64le(const void *p) {
    uint64_t result = 0;
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    memcpy(&result, p, sizeof(result));
#else
    const unsigned char *q = p;
    result |= q[7]; result <<= 8;
    result |= q[6]; result <<= 8;
    result |= q[5]; result <<= 8;
    result |= q[4]; result <<= 8;
    result |= q[3]; result <<= 8;
    result |= q[2]; result <<= 8;
    result |= q[1]; result <<= 8;
    result |= q[0];
#endif
    return result;
}

现在,为什么你不应该像其他答案建议的那样转换偏移指针:首先,因为取消引用未对齐的指针是 UB。并非每个体系结构都支持从任意地址读取大于 8 位的字,即使在那些支持它们的体系结构上,编译器仍可能假设所有取消引用的地址在生成代码时都正确对齐,尤其是在优化下。如果您 运行 使用 UBSan 编写代码,它也会抱怨。

第二个原因是严格别名。 C 语言规定所有内存必须通过指向字符类型(charsigned charunsigned char)的指针或指向存储对象的类型的指针来访问那段记忆;这确保可以假定指向不同类型的指针不会别名(指向同一内存)。在实践中,uint8_t通常是unsigned char的别名,是字符类型,例外地允许别名任何类型;到目前为止,这使得严格的别名问题主要是理论上的。尽管如此,也没有理由冒这个风险,因为避免它是如此容易和便宜。