不依赖未定义行为的内隐转换

问题描述 投票:0回答:1

我使用C语言读取一个 .png 图像文件,如果你不熟悉的是 PNG 编码格式,有用的整数值以 .png 文件的形式是4字节的大二烯整数。

我的电脑是一台小恩典机器,所以要从大恩典的 uint32_t 我从文件中读取的 fread() 到一个我的计算机能理解的小字型,我一直在使用我写的这个小函数。

#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 标志,但我很有信心地认为,我所依赖的是未定义的行为和类型逃逸,而这些行为和类型逃逸在其他系统上可能无法正常工作。

有没有一个标准的函数可以让我调用,把一个大字节的整数转换为本地机器可以理解的整数,或者如果没有,有没有其他更安全的方法来完成这种转换?

c endianness unions type-punning
1个回答
3
投票

我看到OP的代码中没有真正的UB。

可移植性问题:是的。

"在其他系统上可能不那么好用的类型运行 "在OP的C代码中不是问题,但在其他语言中可能会造成麻烦。


然而用一个大的(PNG)endian来代替主机呢?

按地址提取字节(从MSByte的最低地址到LSByte的最高地址--"大 "endian),然后用移位后的字节形成结果。

就像这样。

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.


2
投票

IMO从字节中读取所需的格式会更好,而不是明显的memcpy'ing一个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的系统上也会因为大位移而导致未定义的行为。

现代编译器 足够聪明的优化,这一点,比如说。由clang小安迪产生的汇编。 是。

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

然而我看到gcc 10.1产生的代码要复杂得多 这似乎是一个令人惊讶的遗漏的优化bug


0
投票

这个解决方案并不依赖于访问联合体中的非活动成员,而是依赖于无符号整数位移操作,它可以安全地从大二进制转换为小二进制。反之

#include <stdint.h>

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

0
投票

这段代码读取的是 uint32_t 从一个指针 uchar_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);
    /* ... */
}
© www.soinside.com 2019 - 2024. All rights reserved.