某些数组前/后增量情况下的gcc奇怪行为(已编辑)

时间:2014-05-29 15:23:56

标签: c gcc

我编写的编译器在{}范围内,与C99的语义相匹配。当试图逆向工程gcc如何处理某些未定义的行为时,具体地说,链接变量的前后增量,我注意到如果你将它与修改赋值相结合它会毫无希望地混淆(例如,& #34; * =")和数组访问。 gcc 4.6.3简化了最容易混淆的最简单点。评估(使用和不使用选项-std = c99):

a[0] = 2;
a[0] *= a[0]++;

a[0] = 3.

我错误地记错了标准吗?是否任何使用的前置或后置增量已经未定义,不仅仅是复合表达式中的链式使用?

此外,即使行为未定义',上述似乎是计算结果的特别差的方法,因为我只能看到你如何证明5的结果(= 2) * 2 + 1,我将实现的 - 在赋值语句后的后增量),或6(= 3 * 2,使用变量,然后立即后增加它,并按解析顺序处理 - 解析器是几乎肯定在评估RHS表达式后评估" * =")。对此有何见解 - 从C或C ++的角度来看?

当我尝试将数组与整数表达式边界与增量前后相结合时,我注意到这一点,并实现这真的很难;但是,考虑到gcc的旗舰地位,上述内容似乎有点像一个警察。

这是在Ubuntu 12.04下。

编辑:我应该补充说,如果变量不是数组元素,gcc的行为可以进行逆向工程 - 至少我尝试的所有示例都按如下方式工作:(1)评估所有复合表达式预增量; (2)评估表达; (3)评估所有化合物表达后增量。所以它可能与“真的很难”有关。以上也是。

注意:clang产生的哲学上合理的值为6.我用clang运行了更详细的案例,并且合理地确定它将数组访问和标量情况视为相同,并按照我上面描述的第二个哲学操作合理的方式。

4 个答案:

答案 0 :(得分:4)

分配中的突变(包括读取和变异分配,例如*=)和后增量可以在对突变细胞的值的初始访问之后的表达式的评估中的任何时间发生。 。因此,a[0]*=a[0]++的突变不是相互排序的。显然在这种情况下,gcc选择从左到右进行。

或者更准确地说,表达式:

x *= y++;

可以改写为:

tmp1 = y + 1;        tmp2 = x * y;
x = tmp1;            y = tmp2;

其中列可以按任何顺序交错。

请注意,实际上,tmp个变量都可能是机器寄存器。事实上,可能发生的事情更像是这样:

r1  = y;
r2  = r1 + 1;
r3  = x;
r4  = r3 * r1;
x = r4;
y = r2;

那么为什么最后两个作业按顺序而不是另一个顺序呢?那么,为什么不呢?

显然,这里的混淆是xy是相同的位置,但gcc没有义务注意到这一点。它可能使用基于假设它们是不同位置的优化启发式。

但是假设它注意到它们是相同的。在这种情况下,可以消除一个或另一个分配。此外,还可以消除要存储的临时值的计算。因此,编译器可以选择消除r4 = r3 * r1; x = r4r2 = r1 + 1; y = r2。通过选择消除增量或乘法,自尊优化器会做什么?

答案 1 :(得分:3)

  

我错误地记错了标准吗?是否已经未定义预增量或后增量的使用,而不仅仅是在复合表达式中使用链接?

来自horse's mouth(2011年草案):

6.5表达
...
2如果相对于不同的副作用,对标量对象的副作用未被排序 在相同的标量对象上或使用相同标量的值进行值计算 对象,行为未定。如果有多个允许的排序 表达式的子表达式,如果这样一个未经测序的一方,行为是不确定的 效果发生在任何排序中。 84)

84)此段落呈现未定义的语句表达式,例如
  i = ++i + 1;
  a[i++] = i;
同时允许
  i = i + 1;
  a[i] = i;

未定义表示未定义;编译器没有义务产生有意义的或逻辑的结果(如果它想要,它可以,但它不必);它甚至不需要为每次运行重现相同的结果。 gcc做某事,但考虑到订购操作的所有自由,它可能是非常随机的。

请注意,a[0] *= a[1]++将是明确定义的(前提是a[0]a[1]都不包含陷阱表示。

修改

关于如何从中获得3分,我有一个想法......

  1. r1 <- a[0](评估LHS)
  2. r2 <- a[0](评估RHS)
  3. r1 <- r1 * r2(执行乘法)
  4. a[0] <- r1(写入乘法到LHS的结果)
  5. a[0] <- r2 + 1(对RHS应用副作用)
  6. 的Presto;由于副作用的应用方式,你已经破坏了乘法的结果。 gcc是否真的做了类似的事情是一个悬而未决的问题,无论哪种方式都无关紧要;未定义的行为不必一致或可重复。

答案 2 :(得分:2)

(子)表达式a[0]++具有值和副作用:值为3;副作用是将a[0]的值更改为3。请注意,副作用可以在封闭的序列点之间的任何时间发生(前一个语句的结尾和当前语句的结尾)。

(子)表达式a[0] *= <value>具有值和副作用:值为/* old value of */ a[0] * <value>,副作用正在将a[0]更改为先前的值{{1} }乘以a[0]

请注意,您有两个(子)表达式尝试在序列点之间更改同一对象。 C标准明确规定只允许进行一次此类更改。

如果你正在编写一个编译器,你可以自由地检测这种用法并报告它(带有警告或错误),或者不检测它并做任何事情(甚至什么都没有 - 忽略声明完全没问题)。
任何包括让编译器生成可执行文件&#34; Hello,World!&#34;或者中止编译器本身。

答案 3 :(得分:-1)

它被称为&#34;未定义的行为&#34;。您在同一语句中修改了同一个对象a[0]两次(没有插入序列点),一次使用*=运算符,一次使用++运算符。行为未定义。任何事情都可能发生,包括你的电脑崩溃或更糟。

一旦行为未定义,争论编译器所做的事情是完全没有意义的。这是您代码中的错误。修理它。