通过reinterpret_cast进行未对齐访问

时间:2015-09-15 14:58:53

标签: c++ c++11 language-lawyer

我正在讨论试图通过reinterpret_cast确定C ++中是否允许未对齐访问。我想不是,但我找不到确认或反驳的标准的正确部分。我一直在看C ++ 11,但如果更清楚的话,我会对另一个版本没问题。

未对齐的访问在C11中未定义。 the C11 standard的相关部分(第6.3.2.3段,第7段):

  

指向对象类型的指针可以转换为指向不同对象类型的指针。如果生成的指针未针对引用的类型正确对齐,则行为未定义。

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

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

  

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

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

  

行为,对于格式正确的程序构造和正确的数据,取决于实现

     

[注意:不需要实现来记录发生的行为。本国际标准通常描述了可能的行为范围。 - 结束记录]

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

然而,我与之讨论的人有不同的解释。他们引用§1.9,第5段:

  

执行格式良好的程序的符合实现应该产生与具有相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。但是,如果任何此类执行包含未定义的操作,则此国际标准不要求使用该输入执行该程序的实现(甚至不考虑第一个未定义操作之前的操作)。

由于没有 undefined 行为,他们认为C ++编译器无权假设未发生未对齐访问。

那么,在C ++中通过void unaligned_cp(void* a, void* b) { *reinterpret_cast<volatile uint32_t*>(a) = *reinterpret_cast<volatile uint32_t*>(b); } 安全的未对齐访问是什么?它在说明书(任何版本)中的位置是什么?

编辑:通过“访问”,我的意思是实际加载和存储。像

这样的东西
malloc

如何分配内存实际上在我的范围之外(它适用于可以从任何地方调用数据的库),但是{{1}}和堆栈上的数组都可能是候选者。我不想对内存的分配方式施加任何限制。

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

3 个答案:

答案 0 :(得分:9)

看3.11 / 1:

  

对象类型具有对齐要求(3.9.1,3.9.2),这些要求对可以分配该类型的对象的地址施加限制。

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

*reinterpret_cast<uint32_t*>(a)为例。如果此表达式不会导致UB,则(根据严格别名规则)在此语句之后的给定位置必须存在类型为uint32_t(或int32_t)的对象。对象是否已经存在,或者这个写入是否创建它并不重要。

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

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

答案 1 :(得分:3)

编辑这回答了OP的原始问题,即&#34;正在访问未对齐的指针安全&#34;。自那时起,OP已经将他们的问题编辑为&#34;正在解除引用未对齐的指针安全问题,这是一个更加实际和不太有趣的问题。

在这种情况下,未指定指针值的往返转换结果。在某些有限的情况下(包括对齐),将指向A的指针转换为指向B的指针,然后再返回,即使您没有,也会生成原始指针该位置的B

如果不满足对齐要求,那么指向往返 - 指向A到指针到指针指向A的指针会产生带有未指定值的指针

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

然而,仅仅存储指针不是未定义的行为。

以上C ++引用都没有谈到实际使用指向A的指针作为指向B的指针。使用指向&#34;错误类型的指针&#34;除了极少数情况外,所有情况都是未定义的行为,期间。

此示例涉及创建std::aligned_storage_t<sizeof(T), alignof(T)>。你可以在那个地方建造你的T,它会幸福地生活,即使它真的&#34;是aligned_storage_t<sizeof(T), alignof(T)>。 (但是,您可能必须使用从展示位置new返回的指针才能完全符合标准;我不确定。请参阅严格别名。)

可悲的是,标准在对象生命周期方面有点缺乏。它指的是它,但是在我检查之后并没有很好地定义它。您只能在T生活在某个特定位置时使用T,但这意味着在所有情况下都不明确。

答案 2 :(得分:1)

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

5.2.10,第7段说,假设int的对齐比char更严格,那么char*int*char*的往返行程}为生成的char*生成一个未指定的值。

另一方面,如果您将int*转换为char*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)。指向T的指针有一些限制使用,该指针已正确分配,但其生命周期尚未开始(/ 5和/ 6)。

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

因此声称指向未对齐的int 的理论int*未指向int 。取消引用时,对指针的行为没有限制;通常的解除引用规则提供了指向对象的有效指针的行为(包括int)及其行为方式。

现在我们的其他假设。此处some_ptr没有对int* some_ptr = (int*)(((char*)&some_ints[0])+1);的价值进行限制:int

它不是指向(int*)nullptr的指针,就像int不是指向char*的指针一样。将其回传到0xbaadf00d会导致标准中明确显示未指定值的指针(可能是nullptrsome_ptr)。

标准定义了您必须做的事情。有(几乎?我猜在布尔上下文中评估它必须返回一个bool)标准对char*的行为没有要求,除了将它转换回DROP SEQUENCE IF EXISTS id_seq; CREATE SEQUENCE id_seq; 导致一个未指定的值(指针)。