为什么编译器会假设这些看似相等的指针不同?

时间:2016-03-16 12:31:36

标签: c pointers gcc language-lawyer compiler-optimization

看起来GCC有一些优化认为来自不同翻译单元的两个指针永远不会相同,即使它们实际上是相同的。

代码:

的main.c

#include <stdint.h>
#include <stdio.h>

int a __attribute__((section("test")));
extern int b;

void check(int cond) { puts(cond ? "TRUE" : "FALSE"); }

int main() {
    int * p = &a + 1;
    check(
        (p == &b)
        ==
        ((uintptr_t)p == (uintptr_t)&b)
    );
    check(p == &b);
    check((uintptr_t)p == (uintptr_t)&b);
    return 0;
}

b.c

int b __attribute__((section("test")));

如果我用-O0编译它,它会打印

TRUE
TRUE
TRUE

但是-O1

FALSE
FALSE
TRUE

所以p&b实际上是相同的值,但编译器优化了它们的比较,假设它们永远不会相等。

我无法弄清楚,哪种优化做到了这一点。

它看起来不像严格的别名,因为指针属于一种类型,-fstrict-aliasing选项不会产生这种效果。

这是记录在案的行为吗?或者这是一个错误吗?

5 个答案:

答案 0 :(得分:12)

您的代码中有三个方面导致一般问题:

  1. 将指针转换为整数是实现定义。无法保证转换两个指针以使所有位相同。

  2. uintptr_t保证从指针转换为相同类型然后返回不变(即比较等于原始指针)。但仅此而已。整数值本身不保证比较相等。例如。可能存在具有任意值的未使用位。请参阅标准7.20.1.4

  3. 并且(简要地)两个指针可以仅比较相等,如果它们指向同一个数组或正好在它后面(最后一个条目加一个)或至少一个是 null指针。对于任何其他星座,他们比较不平等。有关具体细节,请参阅标准6.5.9p6

  4. 最后,无法保证变量如何通过工具链放置在内存中(通常是静态变量的链接器,自动变量的编译器)。只有数组或struct(即复合类型)才能保证其元素的排序。

    对于您的示例,6.5.9p7也适用。它基本上将指向非数组对象的指针视为比较大小为1的数组的第一个条目。这样做覆盖增量指针过去对象&a + 1。相关是指针所基于的对象。这是指针a的对象p和指针b的{​​{1}}。其余部分见第6段。

    您的所有变量都不是数组(第6段的最后一部分),因此指针无需比较相等,即使&b也是如此。最后一次&#34; TRUE&#34;可能来自gcc,假设&a + 1 == &b比较返回true。

    众所周知,gcc在严格遵循标准的同时进行了积极的优化。其他编译器更保守,但这会导致代码优化程度降低。 不要尝试&#34;解决&#34;这可以通过禁用优化或其他黑客攻击,但使用明确定义的行为来修复它。这是代码中的一个错误。

答案 1 :(得分:8)

p == &b是一个指针比较,并遵守C标准中的以下规则( 6.5.9 Equality运算符,第4点):

  

两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都指向超过最后一个元素的指针。相同的数组对象,或者一个是指向一个数组对象末尾的指针,另一个是指向不同数组对象的开头的指针,该数组对象恰好跟随地址空间中的第一个数组对象。

(uintptr_t)p == (uintptr_t)&b算术比较,并遵守以下规则( 6.5.9 Equality运算符,第6点):

  

如果两个操作数都具有算术类型,则执行通常的算术转换。当且仅当它们的实部两者相等并且它们的虚部相等时,复数类型的值是相等的。当且仅当它们转换为通常算术转换确定的(复杂)结果类型的结果相等时,来自不同类型域的任何两个算术类型值都相等。

这两个摘录需要与实现完全不同的东西。很明显,在调用后一种类型的情况下,C规范没有要求实现模仿前一种比较的行为,反之亦然。 实现只需遵循此规则( 7.18.1.4能够在C99中保存对象指针的整数类型或在C11中 7.20.1.4 ):

  

[uintptr_t]类型指定一个无符号整数类型,其属性是任何有效的void指针都可以转换为此类型,然后转换回指向void的指针,结果将与原始指针进行比较。

(附录:上述引用并非适用于此情况,因为从int*uintptr_t的转换不涉及void*作为中间步骤。请参阅Hadi's answer for an explanation and citation on this。然而,有问题的转换是实现定义的,您尝试的两个比较不需要表现出相同的行为,这是此处的主要内容。)

作为差异的一个例子,考虑两个指向两个不同地址空间的相同地址的指针。将它们作为指针进行比较不应该返回true,但将它们作为无符号整数进行比较可能会。

&a + 1是一个添加到指针的整数,它遵循以下规则( 6.5.6 Additive operators,point 8 ):

  

当指针中添加或减去具有整数类型的表达式时,   result具有指针操作数的类型。如果指针操作数指向的元素   一个数组对象,并且该数组足够大,结果指向一个偏移的元素   原始元素使得结果和原始的下标不同   数组元素等于整数表达式。换句话说,如果表达式P指向   数组对象的第i个元素,表达式(P)+ N(等效地,N +(P))和   (P)-N(其中N具有值n)分别指向第i + n和第i-n个元素   数组对象,只要它们存在。而且,如果表达式P指向最后一个   数组对象的元素,表达式(P)+1指向一个过去的最后一个元素   数组对象,如果表达式Q指向一个数组对象的最后一个元素,   表达式(Q)-1指向数组对象的最后一个元素。如果两个指针   操作数和结果指向同一个数组对象的元素,或者指向最后一个数组对象的元素   数组对象的元素,评估不得产生溢出;否则,   行为未定义。如果结果指向一个超过数组对象的最后一个元素的那个,那么   不得用作被评估的一元*运算符的操作数。

我相信这段摘录表明,指针加法(和减法)仅针对同一数组对象中的指针或超过最后一个元素的指针定义。 因为(1)a不是数组而且(2)ab不是同一个数组对象的成员,所以在我看来你的指针数学运算调用未定义的行为,你的编译器利用它来假设指针比较返回false。再次如Hadi's answer中指出的那样(与我原来的答案在此时所假设的相反) ,指向非数组对象的指针可以被认为是指向长度为1的数组对象的指针,因此在指向标量的指针中添加一个符合指向一个超过数组末尾的指针。

因此,您的案例似乎属于本答案中提到的第一段摘录的最后一部分,使您的比较定义明确,以便当且仅当两个变量按顺序和升序链接时才评估为真。标准是否适用于您的计划是否属实,并且由实施决定。

答案 2 :(得分:4)

虽然其中一个答案已经被接受,但是接受的答案(以及所有其他答案)都是严重错误的,因为我会解释并回答问题。我将引用相同的C标准,即n1570。

让我们从&a + 1开始。与@Theodoros和@Peter所说的相反,这个表达式定义了行为。要看到这一点,请考虑第6.5.6节第7段和第34段;添加剂操作符&#34;其中说明:

  

出于这些运算符的目的,指向对象的指针   不是数组的元素与指向第一个元素的指针的行为相同   长度为1的数组的元素,其对象的类型为   元素类型。

和第8段(特别是强调部分):

  

添加或减去具有整数类型的表达式时   从指针开始,结果具有指针操作数的类型。如果   指针操作数指向数组对象的元素和数组   足够大,结果指向一个偏离的元素   原始元素使得下标的差异   结果和原始数组元素等于整数表达式。   换句话说,如果表达式P指向一个的第i个元素   数组对象,表达式(P)+ N(等效地,N +(P))和(P)-N   (其中N具有值n)分别指向第i + n和第i   数组对象的第i个元素,只要它们存在即可。而且,如果   表达式P指向数组对象的最后一个元素,即   表达式(P)+1点数组对象的最后一个元素,   如果表达式Q指向一个数组的最后一个元素   对象,表达式(Q)-1指向数组的最后一个元素   宾语。如果指针操作数和结果都指向元素   相同的数组对象,或一个超过数组的最后一个元素   对象,评估不得产生溢出;否则,   行为未定义。如果结果指向最后一个元素   对于数组对象,它不应该用作一元*的操作数   被评估的运算符。

表达式(uintptr_t)p == (uintptr_t)&b包含两部分。从指针到uintptr_t的转换是由第7.20.1.4节定义的 NOT (与@Olaf和@Theodoros所说的相反):

  

以下类型指定带有的无符号整数类型   可以将任何有效指向void的指针转换为此类型的属性,   然后转换回指向void的指针,结果将进行比较   等于原始指针:

     

uintptr_t的

认识到此规则仅适用于void的有效指针非常重要。但是,在这种情况下,我们有一个指向int的有效指针。相关段落可在第6.3.2.3节第1节中找到:

  

指向void的指针可以转换为指向任何对象的指针   类型。指向任何对象类型的指针可以转换为指向   无效又回来;结果应与原始数据相等   指针。

这意味着根据本段和7.20.1.4允许(uintptr_t)(void*)p。但(uintptr_t)p(uintptr_t)&b由第6.3.2.3节第6段规定:

  

任何指针类型都可以转换为整数类型。除了   之前指定的,结果是实现定义的。如果   结果不能用整数类型表示,行为是   未定义。结果不必在任何值的范围内   整数类型。

请注意uintptr_t是整数类型,如上面第7.20.1.4节所述,因此适用此规则。

(uintptr_t)p == (uintptr_t)&b的第二部分是比较平等。如前所述,由于转换结果是实现定义的,因此相等的结果也是实现定义的。无论指针本身是否相等,这都适用。

现在我将讨论p == &b。 @Olaf的答案中的第三点是错误的,@ Theodoros的答案在这个表达方面是不完整的。第6.5.9节&#34;平等运营商&#34;第7段:

  

出于这些运算符的目的,指向对象的指针   不是数组的元素与指向第一个元素的指针的行为相同   长度为1的数组的元素,其对象的类型为   元素类型。

和第6段:

  

两个指针比较相等,当且仅当两个指针都是空指针时,   两者都是指向同一对象的指针(包括指向对象的指针)   以及它的开头的一个子对象)或函数,都是指针   一个超过同一个数组对象的最后一个元素,或者一个是a   指向一个数组对象末尾的指针,另一个指向   指向恰好发生的另一个数组对象的开始的指针   紧跟地址空间中的第一个数组对象。)

与@Olaf所说的相比,使用==运算符比较指针永远不会导致未定义的行为(根据6.5.8第5节的规定,只有在使用<=之类的关系运算符时才会出现这种情况。为简洁起见,我将在此省略。现在,p指向相对于int的下一个a,只有当链接器将&b放置在b中时,它才会等于a二进制文件。否则就有不平等。所以这是依赖于实现的(标准未指定ba的相对顺序)。由于b__attribute__((section("test")))的声明使用了语言扩展,即check(p == &b),因此相对位置确实依赖于J.5和3.4.2的实现(为简洁起见省略)。 / p>

我们得出结论,check((uintptr_t)p == (uintptr_t)&b)和{{1}}的结果与实现有关。所以答案取决于您使用的是哪个版本的编译器。我使用gcc 4.8并使用默认选项进行编译,除了优化级别,我在-O0和-O1情况下得到的输出都是TRUE。

答案 3 :(得分:2)

根据C11 6.5.9 / 6和C11 6.5.9 / 7,如果p == &b1在地址中相邻,则测试a必须提供b空间。

您的示例显示GCC似乎无法满足标准的此要求。

2016年4月26日更新:我的原始答案包含有关修改代码以删除其他潜在UB来源并隔离这一条件的建议。

然而,自从发现这个帖子提出的问题是under review - N2012以来,它已经发现了。

他们的一个建议是p == &b应该是未指定的,并且他们承认GCC确实没有实现ISO C11要求。

所以我的答案中还有剩下的文字,因为不再需要证明编译错误&#34;,因为不符合(无论你是否想把它称为bug)成立了。

答案 4 :(得分:1)

重新阅读您的计划我发现您(可以理解)被优化版本中的事实所困惑

p == &b

是假的,而

(uintptr_t)p == (uintptr_t)&b;

是真的。最后一行表明数值确实相同;那怎么可能p == &b是假的?

我必须承认我不知道。我确信这是一个gcc bug。

在与MM讨论后,我认为如果转换为uintptr_t经过一个中间空指针(我应该在程序中包含它并查看它是否有任何改变),我可以提出以下情况:

因为转化链中的两个步骤int* - &gt; void* - &gt; uintptr_t保证是可逆的,不等int指针在逻辑上不会产生相等的uintptr_t值。 1 (等于uintptr_t值必须转换回相等的int指针,至少改变其中一个,从而违反了保值转换规则。)在代码中(我不是为了平等,只是展示转换和比较):

int a,b, *ap=&a, *bp = &b;

assert(ap != bp);

void *avp = ap, *bvp bp;

uintptr_t ua = (uintptr_t)avp, ub = (uintptr_t)bvp;

// Now the following holds:
// if ap != bp then *necessarily* ua != ub. 
// This is violated by the OP's case (sans the void* step).

assert((int *)(void *)ua == (int*)(void*)ub);

1 这假设uintptr_t不以填充比特的形式携带隐藏信息,填充比特不是在算术比较中评估,而是可能在类型转换中。可以通过CHAR_BIT,UINTPTR_MAX,sizeof(uintptr_t)和一些小提琴来检查.-
出于类似的原因,可以想象两个uintptr_t值比较不同但转换回相同的指针(即如果uintptr_t中的位不用于存储指针值,并且转换确实如此)不要把它们归零)。但这与OP的问题相反。