通过reinterpret_cast进行未对齐的访问

Unaligned access through reinterpret_cast

本文关键字:对齐 访问 reinterpret cast 通过      更新时间:2023-10-16

我正在进行讨论,试图弄清楚在C++中是否允许通过reinterpret_cast进行未对齐的访问。我想不是,但我很难找到标准中正确的部分来证实或反驳这一点。我一直在研究C++11,但如果它更清楚的话,我可以接受另一个版本。

C11中未定义未对齐的访问。C11标准的相关部分(§6.3.2.3,第7段(:

指向对象类型的指针可以转换为指向不同对象类型的指示器。如果结果指针没有正确对齐引用的类型,则行为是未定义的。

由于未对齐访问的行为是未定义的,一些编译器(至少GCC(认为这意味着可以生成需要对齐数据的指令。大多数时候,代码仍然适用于未对齐的数据,因为现在大多数x86和ARM指令都适用于未对准的数据,但有些则不然。特别是,一些向量指令没有,这意味着随着编译器在生成优化指令方面变得更好,与旧版本的编译器一起工作的代码可能无法与新版本一起工作。当然,有些体系结构(如MIPS(在处理未对齐的数据时表现不佳。

当然,C++11更为复杂。§5.2.10第7段规定:

对象指针可以显式转换为不同类型的对象指针。当类型为"pointer to T1"的prvalue v转换为类型为"pointer to cv T2"时,如果T1T2都是标准布局类型(3.9(,并且T2的对齐要求不比T1的对齐要求严格,或者如果其中一种类型是void,则结果为static_cast<cv T2*>(static_cast<cv void*>(v))。将类型为"pointer to T1"的prvalue转换为类型为"指针to T2"(其中T1T2是对象类型,并且T2的对齐要求不比T1的对齐要求严格(并返回到其原始类型会产生原始指针值。任何其他此类指针转换的结果都是未指定的。

请注意,最后一个单词是"未指定",而不是"未定义"。§1.3.25将"未指明行为"定义为:

行为,对于一个格式良好的程序结构和正确的数据,这取决于的实现

[注释:实施不需要记录发生的行为。可能的行为范围通常由本国际标准规定。--结束注释]

除非我遗漏了什么,否则在这种情况下,该标准实际上并没有描述可能的行为范围,这似乎向我表明,一个非常合理的行为是为C实现的行为(至少由GCC实现(:不支持它们。这意味着编译器可以自由地假设未对齐的访问不会发生,并发出可能无法使用未对齐内存的指令,就像它对C.所做的那样

然而,与我讨论这个问题的人却有不同的解释。他们引用了§1.9第5段:

执行格式良好程序的一致性实现应产生与使用相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。然而,如果任何此类执行包含未定义的操作,则本国际标准不对使用该输入执行该程序的实现提出要求(甚至不涉及第一个未定义操作之前的操作(。

由于不存在未定义的行为,他们认为C++编译器无权假设不会发生未对齐的访问。

那么,在C++中,通过reinterpret_cast进行的未对齐访问安全吗?规范(任何版本(中说明了什么?

编辑:所谓"访问",指的是实际加载和存储。类似的东西

void unaligned_cp(void* a, void* b) {
  *reinterpret_cast<volatile uint32_t*>(a) =
    *reinterpret_cast<volatile uint32_t*>(b);
}

内存的分配方式实际上超出了我的范围(这是一个可以用来自任何地方的数据调用的库(,但malloc和堆栈上的数组都可能是候选者。我不想对内存的分配方式施加任何限制。

编辑2在答案中引用来源(,即,C++标准,章节和段落(。

查看3.11/1:

对象类型具有对齐要求(3.9.1、3.9.2(,这些要求对可以分配该类型对象的地址进行了限制。

关于分配一个类型的对象究竟是什么,评论中存在一些争论。然而,我相信,无论讨论如何解决,以下论点都是有效的:

*reinterpret_cast<uint32_t*>(a)为例。如果此表达式不导致UB,则(根据严格的别名规则(在该语句之后的给定位置必须有一个类型为uint32_t(或int32_t(的对象。对象是否已经在那里,或者这篇文章创建了它,都无关紧要。

根据上述标准引用,具有对齐要求的对象只能以正确对齐的状态存在。

因此,任何创建或写入未正确对齐的对象的尝试都会导致UB。

编辑这回答了OP最初的问题,即"正在安全访问未对齐的指针"。此后,OP将他们的问题编辑为"是否安全地取消引用未对齐的指针",这是一个更实用、更不有趣的问题。


在这些情况下,指针值的往返强制转换结果是未指定的。在某些有限的情况下(涉及对齐(,将指向a的指针转换为指向B的指针,,然后再返回,即使该位置没有B,也会产生原始指针

如果不满足对齐要求,那么往返--从指针到A到指针到B再到指针到A会产生一个未指定值的指针。

由于存在无效的指针值,用未指定的值取消引用指针可能会导致未定义的行为。从某种意义上讲,它与*(int*)0xDEADBEEF没有什么不同。

然而,简单地存储该指针并不是未定义的行为。

上面的C++引用都没有谈到实际使用pointer-to-a作为pointer-to-B。在除极少数情况外的所有情况下使用指向"错误类型"的指针都是未定义的行为。

例如,创建一个std::aligned_storage_t<sizeof(T), alignof(T)>。你可以在那个地方构建你的T,它会快乐地生活,即使它"实际上"是aligned_storage_t<sizeof(T), alignof(T)>。(但是,为了完全符合标准,您可能必须使用从放置new返回的指针;我不确定。请参阅严格别名。(

遗憾的是,这个标准在对象生存期方面有点欠缺。它引用了它,但上次我检查时没有很好地定义它。当T居住在某个特定位置时,您只能在该位置使用T,但这意味着什么并不是在所有情况下都清楚。

所有的引号都是关于指针值的,而不是取消引用的行为。

5.2.10,第7段说,假设intchar具有更严格的对准,则char*int*char*的往返行程为得到的char*生成未指定的值。

另一方面,如果将int*转换为char*再转换为int*,则可以保证返回与开始时完全相同的指针。

当你取消引用所说的指针时,它不会谈论你得到了什么。它只是简单地说明,在一种情况下,你必须能够往返。它洗手不干。


假设您有一些int,并且alignof(int) > 1:

int some_ints[3] ={0};

那么你有一个偏移的int指针:

int* some_ptr = (int*)(((char*)&some_ints[0])+1);

我们假设复制这个未对齐的指针目前不会导致未定义的行为。

some_ptr的值未由标准指定。我们会慷慨地假设它实际上指向some_bytes中的某个字节块。

现在我们有一个int*,它指向无法分配int的地方(3.11/1(。在(3.8(中,指向int的指针的使用受到多种限制。通常的使用仅限于指向T的指针,该指针的寿命已开始正确分配(/3(。允许对指向已正确分配但其寿命尚未开始(/5和/6(的T的指针进行一些有限的使用。

无法创建不遵守标准中int对齐限制的int对象。

因此,声称指向未对齐的int的理论int*并不指向int。当取消引用时,对所述指针的行为没有限制;通常的去引用规则提供了指向对象(包括int(的有效指针的行为以及它的行为方式。


现在是我们的其他假设。此处some_ptr的值不受标准int* some_ptr = (int*)(((char*)&some_ints[0])+1);的限制。

它不是指向int的指针,就像(int*)nullptr不是指向int的指针一样。将其往返于char*会导致一个指针,该指针在标准中显式地具有未指定的值(可以是0xbaadf00dnullptr(。

标准定义了你必须做的事情。标准对some_ptr的行为没有任何要求(几乎?我想在布尔上下文中评估它必须返回布尔(,只是将其转换回char*会导致(指针的(未指定值。