不依赖未定义行为的字节顺序转换

Endianness conversion without relying on undefined behavior

我正在使用 C 读取 .png 图像文件,如果您不熟悉 PNG 编码格式,有用的整数值编码在 .png 文件中4 字节大端整数的形式。

我的电脑是一台小端机器,所以要将我从带有 fread() 的文件中读取的大端 uint32_t 转换为我的电脑可以理解的小端,我一直在使用我写的这个小功能:

#include <stdint.h>

uint32_t convertEndian(uint32_t val){
  union{
    uint32_t value;
    char bytes[sizeof(uint32_t)];
  }in,out;
  in.value=val;
  for(int i=0;i<sizeof(uint32_t);++i)
    out.bytes[i]=in.bytes[sizeof(uint32_t)-1-i];
  return out.value;
}

这在我的 x86_64 UNIX 环境中运行得很好,gcc 即使使用 -Wall 标志编译也没有错误或警告,但我很自信我依赖 undefined在其他系统上可能效果不佳的行为和类型双关。

有没有我可以调用的标准函数,它可以可靠地将大端整数转换为本机机器可以理解的整数,或者如果没有,是否有其他更安全的方法来进行这种转换?

我在 OP 的代码中看不到真正的 UB。

可移植性问题:是的。

"type-punning that may not work as well on other systems" 不是 OP 的 C 代码问题,但可能会导致其他语言出现问题。


然而,如果要托管一个大 (PNG) 字节序呢?

按地址提取字节(具有 MSByte 的最低地址到具有 LSByte 的最高地址 - "big" 字节序)并用移位的字节形成结果。

类似于:

uint32_t Endian_BigToHost32(uint32_t val) {
  union {
    uint32_t u32;
    uint8_t u8[sizeof(uint32_t)]; // uint8_t insures a byte is 8 bits.
  } x = { .u32 = val };
  return 
      ((uint32_t)x.u8[0] << 24) |
      ((uint32_t)x.u8[1] << 16) |
      ((uint32_t)x.u8[2] <<  8) |
                 x.u8[3];
}

提示:许多库都有特定的实现函数来有效地实现这一点。示例 be32toh.

IMO 从字节读取到所需格式会更好,而不是显然 memcpy 一个 uint32_t 然后在内部操作 uint32_t。代码可能如下所示:

uint32_t read_be32(uint8_t *src)   // must be unsigned input
{
     return (src[0] * 0x1000000u) + (src[1] * 0x10000u) + (src[2] * 0x100u) + src[3];
}

这类代码很容易出错,因此请确保您是从高知名度的 SO 用户那里获得的。但是,您可能经常看到替代建议 return (src[0] << 24) + (src[1] << 16) + (src[2] << 8) + src[3];,如果 src[0] >= 128 由于有符号整数溢出,这会导致未定义的行为,这是由于不幸的规则,整数提升需要 uint8_t 到有符号 int.并且由于大的移位,还会在具有 16 位 int 的系统上导致未定义的行为。

现代编译器应该足够聪明来优化,例如the assembly produced by clang little-endian 是:

read_be32:                              # @read_be32
    mov     eax, dword ptr [rdi]
    bswap   eax
    ret

但是我看到 gcc 10.1 生成了更复杂的代码,这似乎是一个令人惊讶的遗漏优化错误。

此解决方案不依赖于访问联合体的非活动成员,而是依赖于无符号整数移位操作,该操作可以方便且安全地从 big-endian 转换为 little-endian ,反之亦然相反

#include <stdint.h>

uint32_t convertEndian32(uint32_t in){
  return ((in&0xffu)<<24)|((in&0xff00u)<<8)|((in&0xff0000u)>>8)|((in&0xff000000u)>>24);
}

此代码从大端存储中 uchar_t 的指针读取 uint32_t,与您的体系结构的字节顺序无关。 (代码就好像它正在读取一个基数 256 数字)

uint32_t read_bigend_int(uchar_t *p, int sz)
{
    uint32_t result = 0;
    while(sz--) {
        result <<= 8;   /* multiply by base */
        result |= *p++; /* and add the next digit */
    }
}

如果你打电话,例如:

int main()
{
    /* ... */
    uchar_t buff[1024];
    read(fd, buff, sizeof buff);

    uint32_t value = read_bigend_int(buff + offset, sizeof value);
    /* ... */
}