*i
和u.i
如何在此代码中打印不同的数字,即使i
定义为int *i = &u.i;
?我只能假设我在这里触发UB,但我看不清楚到底是什么。
(ideone demo复制,如果我选择'C'作为语言。但正如@ 2501指出的那样,不是'C99严格'是语言。但是再次,我得到问题{{1} }!)
gcc-5.3.0 -std=c99
(gcc 5.3.0,// 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;
}
,还有-fstrict-aliasing -std=c99 -O2
)
我的理论是-std=c11
是'正确的'答案,因为通过100
- 左值short
写入工会成员的定义是这样的(对于这个平台/字节顺序) /随你)。但我认为优化器没有意识到对*s
的写入可能是别名*s
,因此它认为u.i
是唯一可以影响*i=2;
的行。这是一个合理的理论吗?
如果*i
可以别名*s
,u.i
可以别名u.i
,那么编译器肯定会认为*i
可以别名*s
?别名不应该是“传递性的”吗?
最后,我总是有这样的假设,即严重混叠问题是由于糟糕的铸造造成的。但是这里没有铸造!
(我的背景是C ++,我希望我在这里问一个关于C的合理问题。我的(有限的)理解是,在C99中,通过一个工会成员写作然后通过另一个成员阅读是可以接受的不同类型的。)
答案 0 :(得分:56)
差异由-fstrict-aliasing
优化选项发布。它的行为和可能的陷阱在GCC documentation:
特别注意这样的代码:
union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }
从不同的工会成员那里读书的做法 最近写的(称为“打字”)很常见。即使
-fstrict-aliasing
,允许输入类型 - ,前提是通过联合类型访问内存。因此,上面的代码按预期工作。 见Structures unions enumerations and bit-fields implementation。 但是,此代码可能会 不强>:int f() { union a_union t; int* ip; t.d = 3.0; ip = &t.i; return *ip; }
请注意,完全允许符合标准的实现利用此优化,因为第二个代码示例展示了undefined behaviour。见Olaf's和其他人'答案供参考。
答案 1 :(得分:18)
C standard (i.e. C11, n1570), 6.5p7:
对象的存储值只能由具有以下类型之一的左值表达式访问:
- ...
- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者 一个字符类型。
指针的左值表达式是不是 union
类型,因此此异常不适用。编译器正确利用这种未定义的行为。
使指针的类型指向union
类型,并取消引用相应的成员。这应该有效:
union {
...
} u, *i, *p;
答案 2 :(得分:12)
C标准中未严格说明严格别名,但通常的解释是,只有在通过名称直接访问联合成员时,才允许使用联合别名(取代严格别名)。
对于这背后的理由,请考虑:
void f(int *a, short *b) {
规则的目的是编译器可以假设a
和b
不是别名,并在f
中生成有效的代码。但是,如果编译器必须考虑到a
和b
可能与工会成员重叠的事实,那么它实际上无法做出这些假设。
两个指针是否是函数参数并不重要,严格别名规则不会根据它进行区分。
答案 3 :(得分:7)
此代码确实调用UB,因为您不遵守严格的别名规则。 n1256草案中的C99表示6.5表达式§7:
对象的存储值只能由具有其中一个的左值表达式访问 以下类型:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
- 对应于有效类型的有符号或无符号类型的类型 对象,
- 对应于合格版本的有符号或无符号类型的类型 有效的对象类型,
- 聚合或联合类型,其中包括上述类型之一 成员(包括递归地,子集合或包含的联合的成员)或
- 字符类型。
在*i = 2;
和printf(" *i = %d\n", *i);
之间只修改了一个短对象。在严格别名规则的帮助下,编译器可以自由地假设i
指向的int对象没有被更改,它可以直接使用缓存值而无需从主内存重新加载它。
这显然不是普通人所期望的,但严格的别名规则是为了让优化编译器能够使用缓存值而精确编写的。
对于第二次印刷,工会在6.2.6.1类型表示/一般§7的相同标准中引用:
当值存储在union类型的对象的成员中时,该对象的字节数 表示与该成员不对应但与其他成员对应的表示 取未指定的值。
因为u.s
已存储,u.i
已取值未指定标准
但我们可以在后面的6.5.2.3结构和工会成员§3注82:
中阅读如果用于访问union对象内容的成员与上次使用的成员不同 在对象中存储一个值,该值的对象表示的相应部分被重新解释 作为6.2.6中描述的新类型中的对象表示(有时称为“类型”的过程) 惩罚“)。这可能是一个陷阱表示。
虽然注释不是规范性的,但它们确实可以更好地理解标准。当u.s
通过*s
指针存储时,对应于short的字节已更改为2值。假设有一个小端系统,因为100小于short的值,作为int的表示现在应该是2,因为高阶字节是0。
TL / DR:即使不是规范性的,注释82应该要求在x86或x64系列的小端系统上,printf("u.i = %d\n", u.i);
打印2.但是根据严格的别名规则,仍然允许编译器假设i
指向的值未更改,可能会打印100
答案 4 :(得分:6)
你正在探讨一个有争议的C标准领域。
这是严格的别名规则:
对象的存储值只能由左值访问 具有以下类型之一的表达式:
- 与对象的有效类型兼容的类型
- 与对象的有效类型兼容的类型的限定版本,
- 与对应的有符号或无符号类型的类型 对象的有效类型,
- 与对象的有效类型的限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),
- 字符类型。
(C2011,6.5 / 7)
左值表达式*i
的类型为int
。左值表达式*s
的类型为short
。这些类型彼此不兼容,也不兼容任何其他特定类型,严格别名规则也不提供任何其他替代方法,如果指针有别名,则允许两个访问都符合。
如果至少有一个访问不符合,那么行为是未定义的,因此您报告的结果 - 或者实际上任何其他结果 - 完全可以接受。在实践中,编译器必须生成使用printf()
调用重新排序赋值的代码,或者使用来自寄存器的先前加载的*i
值而不是从内存中重新读取它的代码,或类似的东西
上述争议的产生是因为人们有时会指向脚注 95:
如果用于读取union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将被重新解释为对象表示形式。 6.2.6中描述的新类型(有时称为''punning''的过程)。这可能是陷阱表示。
脚注是信息性的,但不是规范性的,所以如果它们发生冲突,毫无疑问哪些文本会胜出。就个人而言,我只是将脚注作为实施指南,澄清了工会成员存储重叠这一事实的含义。
答案 5 :(得分:5)
看起来这是优化器发挥其魔力的结果。
使用-O0
,两行都按预期打印100(假设为little-endian)。使用-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)也不会抱怨此代码。
答案 6 :(得分:1)
无论是偶然还是设计,C89都包含以两种不同方式解释的语言(以及其间的各种解释)。问题在于何时需要编译器识别用于一种类型的存储可能通过另一种类型的指针访问。在C89基本原理给出的示例中,在全局变量(显然不是任何联合的一部分)和指向不同类型的指针之间考虑别名,并且代码中没有任何内容表明可能发生别名
一种解释严重削弱了语言,而另一种解释则限制使用某些优化来“不符合”#34;模式。如果那些没有给予他们优先考虑的优先级的人已经写了C89来明确地与他们的解释相匹配,那么标准的那些部分会被广泛谴责,并且会有一些明确的认可非C的破坏方言,这将尊重给定规则的非严重解释。
不幸的是,发生的事情是由于规则显然不要求编译器编写者应用严格的解释,大多数编译器编写者多年来只是以一种方式解释规则,这种方式保留了使C对系统有用的语义。编程;程序员没有任何理由抱怨标准并没有强制要求编译器表现得合理,因为从他们的角度来看,尽管标准的邋iness,他们应该这样做是显而易见的。然而,有些人坚持认为,既然标准一直允许编译器处理一个语义弱化的Ritchie系统编程语言子集,那么就没有理由要求符合标准的编译器。处理其他任何事情。
这个问题的合理解决方案是认识到C用于各种各样的目的,应该有多种编译模式 - 一种必需的模式会处理所有地址被访问的所有访问,就像它们读取和写入直接底层存储,并且将与期望任何级别的基于指针的类型惩罚支持的代码兼容。另一种模式可能比C11更具限制性,除非代码明确使用指令来指示何时何地需要重新解释或回收用作一种类型的存储以用作另一种类型。其他模式可以允许一些优化,但支持一些可以在更严格的方言下破解的代码;没有特定方言支持的编译器可以用更明确的别名行为替换一个。