不依赖未定义行为的字节顺序转换
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);
/* ... */
}
我正在使用 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);
/* ... */
}