违反 C 中的严格别名,即使没有任何转换?

Violating of strict-aliasing in C, even without any casting?

即使 i 被定义为 int *i = &u.i;*iu.i 如何在这段代码中打印不同的数字?我只能假设我在这里触发了 UB,但我看不出具体情况。

(ideone demo 复制如果我 select 'C' 作为语言。但正如@2501 指出的那样,如果 'C99 strict' 是语言,则不会。但是话又说回来,我遇到了 gcc-5.3.0 -std=c99 的问题!)

// gcc       -fstrict-aliasing -std=c99   -O2
union
{   
    int i;
    short s;
} u;

int     * i = &u.i;
short   * s = &u.s;

int main()
{   
    *i  = 2;
    *s  = 100;

    printf(" *i = %d\n",  *i); // prints 2
    printf("u.i = %d\n", u.i); // prints 100

    return 0;
}

(gcc 5.3.0, -fstrict-aliasing -std=c99 -O2, -std=c11)

我的理论是 100 是 'correct' 答案,因为通过 short-左值 *s 写入联合成员的定义是这样的(对于这个platform/endianness/whatever)。但我认为优化器没有意识到写入 *s 可以为 u.i 添加别名,因此它认为 *i=2; 是唯一可以影响 *i 的行。这是一个合理的理论吗?

如果*s可以作为u.i的别名,而u.i可以作为*i的别名,那么编译器肯定会认为*s可以作为[=11]的别名=]?别名不应该是 'transitive' 吗?

最后,我一直认为严格别名问题是由错误的转换引起的。但是这里没有铸造!

(我的背景是 C++,我希望我在这里问一个关于 C 的合理问题。我的(有限的)理解是,在 C99 中,通过一个联合成员编写然后通过读取是可以接受的另一个不同类型的成员。)

看起来这是优化器施展魔法的结果。

对于 -O0,两行都按预期打印 100(假设是小端)。对于 -O2,正在进行一些重新排序。

gdb 给出以下输出:

(gdb) start
Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14.
Starting program: /tmp/x1
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000

Temporary breakpoint 1, main () at /tmp/x1.c:14
14      {
(gdb) step
15          *i  = 2;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
15          *i  = 2;
(gdb)
16          *s  = 100;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
 *i = 2
19          printf("u.i = %d\n", u.i); // prints 100
(gdb)
u.i = 100
22      }
(gdb)
0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6
(gdb)

发生这种情况的原因,正如其他人所说,是因为通过指向另一种类型的指针访问一种类型的变量是未定义的行为,即使所讨论的变量是联合的一部分。所以在这种情况下,优化器可以随心所欲。

其他类型的变量只能通过保证明确定义行为的联合直接读取。

奇怪的是,即使使用 -Wstrict-aliasing=2,gcc(从 4.8.4 开始)也不会抱怨这段代码。

差异是由 -fstrict-aliasing 优化选项发出的。 GCC documentation:

中描述了它的行为和可能的陷阱

Pay special attention to code like this:

      union a_union {
        int i;
        double d;
      };

      int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

The practice of reading from a different union member than the one most recently written to (called “type-punning”) is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. So, the code above works as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:

      int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

请注意,符合标准的实现完全可以利用此优化,因为第二个代码示例展示了 undefined behaviour. See 和其他人的答案以供参考。

您正在探索 C 标准中一个有争议的领域。

这是严格的别名规则:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union),
  • a character type.

(C2011, 6.5/7)

左值表达式 *i 的类型为 int。左值表达式 *s 的类型为 short。这些类型彼此不兼容,也不与任何其他特定类型兼容,严格的别名规则也没有提供任何其他替代方案,如果指针被别名,则允许两种访问都符合。

如果至少有一个访问不符合要求,则行为未定义,因此您报告的结果——或者实际上任何其他结果——是完全可以接受的。实际上,编译器必须生成使用 printf() 调用重新排序赋值的代码,或者使用先前从寄存器加载的 *i 值而不是从内存中重新读取它的代码,或类似的东西.

上述争议的产生是因为人们有时会指向footnote 95:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

脚注是信息性的,但不是规范性的,因此如果它们发生冲突,以哪个文本为准毫无疑问。就个人而言,我将脚注简单地作为实施指南,阐明联合成员存储重叠这一事实的含义。

C standard (i.e. C11, n1570), 6.5p7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • ...
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or a character type.

你的指针的左值表达式是不是union类型,因此这个例外不适用。编译器正确地利用了这个未定义的行为。

使指针的类型指向 union 类型并取消对相应成员的引用。这应该有效:

union {
    ...
} u, *i, *p;

此代码确实调用了 UB,因为您不遵守严格的别名规则。 C99 的 n1256 草案在 6.5 表达式 §7 中声明:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
— a character type.

*i = 2;printf(" *i = %d\n", *i);之间只修改了一个短对象。在严格的别名规则的帮助下,编译器可以自由地假设i指向的int对象没有被改变,它可以直接使用一个缓存的值而不用从主存中重新加载。

这显然不是正常人所期望的,但严格的别名规则是为了允许优化编译器使用缓存值而精确编写的。

对于第二次打印,联合在 6.2.6.1 类型表示/一般 §7 中的同一标准中被引用:

When a value is stored in a member of an object of union type, the bytes of the object representation that do not correspond to that member but do correspond to other members take unspecified values.

因此,由于 u.s 已被存储,u.i 取了一个值 标准未指定

但我们可以稍后阅读 6.5.2.3 结构和联合成员 §3 注释 82:

If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called "type punning"). This might be a trap representation.

尽管注释不是规范性的,但它们确实可以让人们更好地理解标准。当通过*s指针存储了u.s时,short对应的字节已经变为2的值。假设一个小端系统,因为 100 小于 short 的值,作为 int 的表示现在应该是 2,因为高位字节是 0。

TL/DR:即使不规范,注释 82 也应该要求在 x86 或 x64 系列的小端系统上,printf("u.i = %d\n", u.i); 打印 2。但是根据严格的别名规则,仍然允许编译器假设 i 指向的值没有改变,并且可以打印 100

严格别名在 C 标准中未明确规定,但通常的解释是联合别名(取代严格别名)仅在联合成员通过名称直接访问时才允许。

考虑以下原因:

void f(int *a, short *b) { 

该规则的目的是编译器可以假定 ab 不使用别名,并在 f 中生成高效代码。但是,如果编译器必须考虑到 ab 可能是重叠的联合成员这一事实,它实际上无法做出这些假设。

这两个指针是否是函数参数并不重要,严格的别名规则不会以此为基础进行区分。

无论是偶然还是故意,C89 包含以两种不同方式解释的语言(以及其间的各种解释)。问题在于何时应该要求编译器识别用于一种类型的存储可以通过另一种类型的指针访问。在 C89 基本原理中给出的示例中,别名被认为是全局变量 之间的别名,它显然不属于任何联合 和指向不同类型的指针,并且代码中没有任何内容表明可能会出现别名。

一种解释严重削弱了语言,而另一种解释将某些优化的使用限制在 "non-conforming" 模式。如果那些没有将他们的首选优化给予 second-class 状态的人编写了 C89 以明确匹配他们的解释,那么标准的那些部分将被广泛谴责并且会有某种明确的认可C 的一个完整的方言,它将尊重给定规则的非破坏性解释。

不幸的是,由于规则显然不要求编译器编写者应用严重的解释,大多数编译器编写者多年来只是以一种保留使 C 语言对系统有用的语义的方式简单地解释规则编程;程序员没有任何理由抱怨标准没有强制要求编译器行为合理,因为从他们的角度来看,尽管标准草率,但每个人都应该这样做是显而易见的。然而,与此同时,一些人坚持认为,由于该标准始终允许编译器处理 Ritchie 系统编程语言的语义弱化子集,因此没有理由期望符合标准的编译器处理任何其他内容。

这个问题的明智解决方案是认识到 C 用于多种多样的目的,因此应该有多种编译模式——一种必需的模式将处理所有访问其地址的所有内容,就好像它们读取和直接写入底层存储,并将与期望 any 级别的基于指针的类型双关支持的代码兼容。另一种模式可能比 C11 更严格,除非代码明确使用指令来指示何时何地已用作一种类型的存储需要重新解释或回收以用作另一种类型。其他模式将允许进行一些优化,但支持一些在更严格的方言下会中断的代码;对特定方言没有特定支持的编译器可以用定义更明确的别名行为来替代。