Aar*_*aid 58 c strict-aliasing
怎么能*i和u.i这个代码打印不同的数字,即使i被定义为int *i = &u.i;?我只能假设我在这里触发UB,但我看不清楚到底是怎么回事.
(如果我选择'C'作为语言,ideone demo会复制.但正如@ 2501指出的那样,如果'C99严格'是语言,那就不行了.但话又说回来,我得到了问题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;
}
Run Code Online (Sandbox Code Playgroud)
(gcc 5.3.0,with -fstrict-aliasing -std=c99 -O2,with with -std=c11)
我的理论是100'正确'答案,因为通过short-lvalue 对union成员的写入*s被定义为(对于这个平台/ endianness/whatever).但我认为优化器没有意识到写入*s可以别名u.i,因此它认为这*i=2;是唯一可以影响的行*i.这是一个合理的理论吗?
如果*s可以别名u.i,并且u.i可以别名*i,那么编译器肯定会认为*s可以别名*i吗?别名是否应该"传递"?
最后,我总是有这样的假设:严格的混叠问题是由于糟糕的铸造造成的.但是这里没有铸造!
(我的背景是C++,我希望我在这里问一个关于C的合理问题.我的(有限的)理解是,在C99中,通过一个工会成员写一个然后读另一个不同的成员是可以接受的.类型.)
Grz*_*ski 56
差异是由-fstrict-aliasing优化选项发出的.它的行为和可能的陷阱在GCC文档中描述:
特别注意这样的代码:
Run Code Online (Sandbox Code Playgroud)union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }从不同的工会成员阅读的做法比最近写的那个(称为"打字式")很常见.即使使用
-fstrict-aliasing,只要通过union类型访问内存,就允许类型惩罚.因此,上面的代码按预期工作.请参阅结构联合枚举和位字段实现.但是,此代码可能不会:Run Code Online (Sandbox Code Playgroud)int f() { union a_union t; int* ip; t.d = 3.0; ip = &t.i; return *ip; }
请注意,完全允许符合标准的实现利用此优化,因为第二个代码示例展示了未定义的行为.请参阅奥拉夫和其他人的答案以供参考.
too*_*ite 18
对象的存储值只能由具有以下类型之一的左值表达式访问:
- ...
- 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员)或字符类型.
指针的左值表达式不是 union类型,因此不适用此异常.编译器正确利用这种未定义的行为.
使指针的类型指向union类型,并使用相应的成员取消引用.这应该工作:
union {
...
} u, *i, *p;
Run Code Online (Sandbox Code Playgroud)
M.M*_*M.M 12
C标准中未严格说明严格别名,但通常的解释是,只有在通过名称直接访问联合成员时,才允许使用联合别名(取代严格别名).
对于这背后的理由,请考虑:
void f(int *a, short *b) {
Run Code Online (Sandbox Code Playgroud)
规则的目的是编译器可以假设a和b不使用别名,并生成有效的代码f.但是,如果编译器必须考虑到a并且b可能与工会成员重叠的事实,它实际上无法做出这些假设.
无论两个指针是否是函数参数都是无关紧要的,严格别名规则不会基于此区分.
此代码确实调用UB,因为您不遵守严格的别名规则.n1256草案中的C99表示6.5表达式§7:
对象的存储值只能由具有以下类型之一的左值表达式访问:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
-与对象的有效类型对应的有符号或无符号类型的类型,
- 对应于对象的有效类型的限定版本的有符号或无符号类型,
- 包含一个类型的聚合或联合类型其成员中的上述类型(包括递归地,子聚合或包含的联合的成员),或
- 字符类型.
在*i = 2;和之间printf(" *i = %d\n", *i);只修改一个短对象.在严格别名规则的帮助下,编译器可以自由地假设指向的int对象i没有被更改,并且它可以直接使用缓存值而无需从主内存重新加载它.
它显然不是普通人所期望的,但是严格的别名规则被精确编写以允许优化编译器使用缓存值.
对于第二次印刷,工会在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
你正在探讨一个有争议的C标准领域.
这是严格的别名规则:
对象的存储值只能由具有以下类型之一的左值表达式访问:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
- 与对象的有效类型对应的有符号或无符号类型的类型,
- 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,
- 一种聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),
- 一个字符类型.
(C2011,6.5/7)
左值表达式*i具有类型int.左值表达式*s具有类型short.这些类型彼此不兼容,也不兼容任何其他特定类型,严格别名规则也不提供任何其他替代方案,如果指针有别名,则允许两个访问一致.
如果至少有一个访问不符合,那么行为是未定义的,因此您报告的结果 - 或者实际上任何其他结果 - 是完全可以接受的.实际上,编译器必须生成用printf()调用重新排序赋值的代码,或者使用先前*i从寄存器加载的值而不是从内存重新读取它的代码,或类似的东西.
上述争议的产生是因为人们有时会指出脚注 95:
如果用于读取union对象的内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型中的对象表示形式在6.2.6中描述(一个过程有时被称为''punning'').这可能是陷阱表示.
脚注是信息性的,但不是规范性的,所以如果它们发生冲突,毫无疑问哪些文本会获胜.就个人而言,我只是将脚注作为实施指南,澄清了工会成员存储重叠这一事实的含义.
看起来这是优化器发挥其魔力的结果.
使用时-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)
Run Code Online (Sandbox Code Playgroud)
正如其他人所说,发生这种情况的原因是因为通过指向另一种类型的指针访问一种类型的变量是未定义的行为,即使有问题的变量是联合的一部分.因此,在这种情况下,优化器可以自由地执行.
另一种类型的变量只能通过联合直接读取,联合可以保证定义良好的行为.
令人好奇的是,即使使用-Wstrict-aliasing=2,gcc(截至4.8.4)也不会抱怨此代码.