只要你没有取消引用它,是否可以很好地定义一个未对齐的指针?

Jer*_*ner 35 c pointers alignment language-lawyer

我有一些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!
Run Code Online (Sandbox Code Playgroud)

......显然不会在ARM-land中飞行,所以我修改它看起来更像这样:

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
Run Code Online (Sandbox Code Playgroud)

我的问题(从语言 - 律师的角度来看)是:我的"ARM修复"方法是否在C语言规则下定义明确?

我担心的是,即使只是有一个misaligned-int32_t指针也可能足以调用未定义的行为,即使我从来没有直接取消引用它.(如果我关心的是有效的,我想我可以修复通过改变问题pNextWord从的类型(const int32_t *)(const char *)的,但我宁愿不这样做,除非它实际上必须这样做,因为这将意味着做手工一些指针跨度计算)

Ant*_*ala 21

不,新代码仍然有未定义的行为.C11 6.3.2.3p7:

  1. 指向对象类型的指针可以转换为指向不同对象类型的指针.如果对于引用的类型,结果指针未正确对齐(68),则行为未定义.[...]

它没有说出解除引用指针的任何内容 - 即使转换具有未定义的行为.


实际上,您认为ARM安全的修改后的代码可能不是英特尔安全的 - 众所周知,编译会为英特尔生成可能导致未对齐访问崩溃的代码.虽然不在链接的情况下,但可能只是一个聪明的编译器可以将转换作为地址确实对齐并使用专用代码的证明memcpy.


除此之外,您的第一个摘录也会受到严格的别名违规的影响.C11 6.5p7:

  1. 对象的存储值只能由具有以下类型之一的左值表达式访问:88)
    • 与对象的有效类型兼容的类型,
    • 与对象的有效类型兼容的类型的限定版本,
    • 与对象的有效类型对应的有符号或无符号类型的类型,
    • 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,
    • 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),或者
    • 一个字符类型.

由于数组buf[2048]是静态类型的,char因此每个元素都是,因此元素的有效类型是char; 您可以将数组的内容作为字符访问,而不是作为int32_ts访问.

即使

int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);
Run Code Online (Sandbox Code Playgroud)

有未定义的行为.

  • 抛开对齐,我相信访问被很好地定义为`buf`的内容*不被*作为`int`访问.`pNextWord`在传递给`memcpy`函数时转换为`void*`,然后以安全的方式复制字节. (2认同)

lee*_*ing 8

要在编译器/平台之间安全地解析多字节整数,可以提取每个字节,并根据字节序将它们组装为整数.例如,要从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;
Run Code Online (Sandbox Code Playgroud)

  • 虽然此代码也有未定义的行为:D(但例如GCC保证正确的行为).`b0`将被提升为*signed int*然后1可能会转移到32位`int`的符号位 - 更好地将所有b0 - b3声明为`uint32_t`. (7认同)
  • 重构示例代码,感谢@ Antti Haapala的通知,我的大部分工作都是在linux/windows上,^^. (3认同)

sup*_*cat 5

某些编译器可能会假设任何指针都不会保存与其类型不正确对齐的值,并执行依赖于该值的优化。作为一个简单的例子,请考虑:

void copy_uint32(uint32_t *dest, uint32_t *src)
{
  memcpy(dest, src, sizeof (uint32_t));
}
Run Code Online (Sandbox Code Playgroud)

如果 和 都dest保存src32 位对齐地址,则即使在不支持未对齐访问的平台中,上述函数也可以优化为一次加载和一次存储。但是,如果该函数已声明为接受 类型的参数void*,则在未对齐的 32 位访问的行为与字节访问、移位和按位操作序列不同的平台上将不允许进行此类优化。