我有一些C代码可以解析来自网络的打包/未填充的二进制数据。
该代码在/ Intel / x86下运行正常,但是当我在ARM下编译时,通常会崩溃。
您可能已经猜到,罪魁祸首是未对齐的指针-特别是,解析代码会做类似这样的可疑事情:
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]); // misaligned access -- can crash under ARM!
...这显然不会在ARM领域飞起来,所以我对其进行了修改,使其看起来像这样:
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord)); // slower but ARM-safe
我的问题(从语言律师的角度)是:我的“ ARM固定”方法在C语言规则下是否定义明确?
我担心的是,即使我从未真正直接取消引用它,也许即使具有misaligned-int32_t指针也可能足以调用未定义的行为。 (如果我的顾虑是正确的,我想我可以通过将pNextWord
的类型从(const int32_t *)
更改为(const char *)
来解决此问题,但是我宁愿不这样做,除非确实有必要这样做,因为这意味着需要手动执行一些指针跨步算法)
答案 0 :(得分:21)
否,新代码仍然具有未定义的行为。 C11 6.3.2.3p7:
- 指向对象类型的指针可以转换为指向不同对象类型的指针。如果对于所引用的类型,结果指针未正确对齐68),则行为未定义。 [...]
关于取消引用指针,它什么也没说,即使转换具有不确定的行为。
实际上,您假设修改后的代码对 ARM 是安全的,甚至对 Intel 也不安全-已知编译器会为Intel that can crash on unaligned access生成代码。尽管不是在链接的情况下,但聪明的编译器可以将转换作为地址确实对齐并为memcpy
使用专用代码的证明。 >
除了对齐方式,您的第一节摘录也遭受了严格的混叠违规。 C11 6.5p7:
- 对象应仅通过具有以下类型之一的左值表达式访问其存储值:88)
- 与对象的有效类型兼容的类型
- 与有效类型兼容的合格版本 对象
- 是有符号或无符号类型的类型 对应于对象的有效类型
- 一种类型 是与合格版本对应的有符号或无符号类型 对象的有效类型
- 集合或联合类型 在其成员中包括上述类型之一 (包括(递归地)子集合的成员或包含的成员 联合),或
- 一种字符类型。
由于数组buf[2048]
的类型为静态 ,每个元素均为char
,因此元素的有效类型为char
;您可以仅作为字符而不是int32_t
访问数组的内容。
即甚至
int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);
具有不确定的行为。
答案 1 :(得分:7)
要在编译器/平台之间安全地解析多字节整数, 您可以提取每个字节,然后根据字节序将它们组合为整数。例如,要从big-endian缓冲区读取4字节整数:
uint8_t* buf = any address;
uint32_t val = 0;
uint32_t b0 = buf[0];
uint32_t b1 = buf[1];
uint32_t b2 = buf[2];
uint32_t b3 = buf[3];
val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
答案 2 :(得分:3)
某些编译器可能会假设没有指针会保存一个未正确针对其类型对齐的值,并执行依赖于该值的优化。作为一个简单的示例,请考虑:
void copy_uint32(uint32_t *dest, uint32_t *src)
{
memcpy(dest, src, sizeof (uint32_t));
}
如果dest
和src
都具有32位对齐的地址,则即使在不支持不对齐访问的平台中,上述功能也可以优化为一个负载和一个存储。但是,如果已声明该函数接受类型为void*
的参数,则在未对齐的32位访问行为与字节访问,移位和按位序列不同的平台上,将不允许这种优化操作。
答案 3 :(得分:1)
正如Antti Haapala的答案中提到的那样,仅在结果指针未正确对齐时将指针转换为另一种类型会导致C标准第6.3.2.3p7节中的未定义行为。
您修改后的代码仅使用pNextWord
传递给memcpy
,在这里它被转换为void *
,因此您甚至不需要类型为uint32_t *
的变量。只需将要读取的缓冲区中第一个字节的地址传递到memcpy
。那么您完全不必担心对齐问题。
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));