在C中违反严格别名,即使没有任何铸造?
*i
和ui
如何在这个代码中打印不同的数字,即使i
被定义为int *i = &u.i;
? 我只能假设我在这里触发了UB,但我看不出如何。
(如果我select'C'作为语言, ideone demo会复制,但@ 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("ui = %d\n", ui); // prints 100 return 0; }
(gcc 5.3.0,使用-fstrict-aliasing -std=c99 -O2
,也使用-std=c11
)
我的理论是, 100
是“正确的”答案,因为通过short
值*s
写入联盟成员被定义为这样(对于这个平台/sorting/无论)。 但我认为优化器没有意识到写入*s
可以别名ui
,因此它认为*i=2;
是唯一可以影响*i
。 这是一个合理的理论吗?
如果*s
可以别名ui
,并且ui
可以别名*i
,那么编译器当然应该认为*s
可以别名*i
? 不应该别名是“传递”?
最后,我总是有这样的假设:严格的锯齿问题是由不良铸造造成的。 但是这里没有铸造!
(我的背景是C ++,我希望我在这里问一个关于C的合理问题,我的(有限的)理解是,在C99中,通过一个联合成员编写然后通过另一个不同的成员types。)
该差异是由-fstrict-aliasing
优化选项发出的。 其行为和可能的陷阱在GCC文档中有描述:
要特别注意这样的代码:
union a_union { int i; double d; }; int f() { union a_union t; td = 3.0; return ti; }
从不同的工会会员阅读的做法比最近写的(称为“打字”)是常见的。 即使使用
-fstrict-aliasing
, 只要内存是通过联合types访问的 ,则允许使用types-fstrict-aliasing
。 所以,上面的代码按预期工作。 请参见结构联合枚举和位字段实现 。 但是,这段代码可能不会 :int f() { union a_union t; int* ip; td = 3.0; ip = &t.i; return *ip; }
请注意,符合的实现完全可以利用这种优化,因为第二个代码示例展现出未定义的行为 。 见奥拉夫和其他人的回答供参考。
C标准(即C11,n1570),6.5p7 :
对象的存储值只能由具有以下types之一的左值expression式访问:
- …
- 在其成员(包括recursion地,子成员或包含的联盟的成员)中包括上述types之一的聚集或联合types或字符types。
指针的左值expression式不是 union
types,因此这个例外不适用。 编译器正确地利用这个未定义的行为。
将指针的types指针指向union
types,并对各个成员进行解引用。 这应该工作:
union { ... } u, *i, *p;
C标准中没有严格的别名,但通常的解释是,只有当工会成员直接被姓名访问时,才允许使用联合别名(取代严格的别名)。
有关这个考虑背后的理由:
void f(int *a, short *b) {
规则的意图是编译器可以假定a
和b
不是别名,并在f
生成高效的代码。 但是,如果编制者必须考虑到a
和b
可能与工会成员重叠的事实,那么它实际上不能作出这些假设。
两个指针是不是函数参数都不重要,严格的锯齿规则根本不区分。
这段代码确实调用了UB,因为你不尊重严格的别名规则。 n1256 C99州草案6.5expression式§7:
对象的存储值只能由具有以下types之一的左值expression式访问:
– 与对象的有效types兼容的types,
– 与对象的有效types兼容的types的限定版本,
– 与对象的有效types对应的有符号或无符号types,
– 与对象的有效types的限定版本对应的有符号或无符号types,
– 在其成员中包括上述types之一的集合或联合types(包括recursion地,子集合的成员或包含的联合),或者
– 一个字符types。
在*i = 2;
和printf(" *i = %d\n", *i);
只有一个短的对象被修改。 在严格别名规则的帮助下,编译器可以自由地假定i
所指向的int对象没有被改变,并且可以直接使用caching值而不用从主存储器重新加载。
这显然不是一个正常人所期待的,但严格的锯齿规则是精确编写的,以允许优化编译器使用caching值。
对于第二个印刷品,联合体在6.2.6.1types表示/通用§7中以相同标准引用:
当一个值存储在uniontypes的对象的成员中时,不与该成员相对应但与其他成员相对应的对象表示的字节取未指定的值。
所以,当us
被储存起来的时候, ui
已经采取了标准没有指定的价值
但我们可以在6.5.2.3结构和工会成员§3注82:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不相同,则该值的对象表示的相应部分将被重新解释为新types中的对象表示forms,如如6.2.6所述(一个有时称为“types双关”的过程)。 这可能是一个陷阱代表。
尽pipe笔记不是规范的,但它们确实可以更好地理解标准。 当us
通过*s
指针存储*s
,对应于一个short的字节已经改变为2的值。 假定一个小端系统,因为100小于一个短的值,所以作为一个int的表示现在应该是2,因为高阶字节是0。
TL / DR:即使不是规范的,注释82应该要求在x86或x64系列的小端系统上, printf("ui = %d\n", ui);
打印2.但是,根据严格的锯齿规则,编译器仍然可以假定i
指向的值没有改变,可以打印100
你正在探索一个有点争议的C标准领域。
这是严格的别名规则:
对象的存储值只能由具有以下types之一的左值expression式访问:
- 与对象的有效types兼容的types,
- 与该对象的有效types兼容的types的限定版本,
- 对应于对象的有效types的有符号或无符号types,
- types是对应于对象的有效types的限定版本的有符号或无符号types,
- 包括其成员之一(包括recursion地,子成员或包含工会的成员)中的上述types之一的集合体或联合体types,
- 一个字符types。
(C2011,6.5 / 7)
左值expression式*i
有typesint
。 左值expression式*s
typesshort
。 这些types彼此不兼容,也不兼容任何其他特定types,严格别名规则也不提供任何其他替代方法,如果指针是别名,则允许两个访问一致。
如果至less有一个访问是不符合的,那么行为是不确定的,所以你报告的结果 – 或者其他任何结果都是完全可以接受的。 实际上,编译器必须产生用printf()
调用重新赋值赋值的代码,或者从寄存器中使用先前加载的*i
值,而不是从内存或其他类似的东西中重新读取。
前面提到的争议是因为人们有时会指出脚注 95:
如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不相同,则该值的对象表示forms的适当部分将被重新解释为新types中的对象表示forms,如如6.2.6所述(一个有时称为“types双关”的过程)。 这可能是一个陷阱代表。
脚注是信息性的,但是不是规范性的,所以如果它们发生冲突,那么文本就没有问题了。 就我个人而言,我只是将脚注作为实施指导,明确了存储工会成员的重叠意义。
看起来这是优化器做魔法的结果。
用-O0
,这两行按预期打印100(假设小端)。 用-O2
,有一些重新sorting。
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("ui = %d\n", ui); // prints 100 (gdb) ui = 100 22 } (gdb) 0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6 (gdb)
正如其他人所说的,这种情况发生的原因是因为通过指向另一种types的指针来访问一个types的variables是未定义的行为,即使所讨论的variables是联合的一部分。 所以在这种情况下优化器可以自由地按照自己的意愿去做。
其他types的variables只能通过一个保证良好定义的行为的联合直接读取。
有趣的是,即使使用-Wstrict-aliasing=2
,gcc(从4.8.4开始)也不会抱怨这个代码。
无论是意外还是通过devise,C89都包括以两种不同方式解释的语言(以及各种解释)。 问题是什么时候应该要求编译器识别用于一种types的存储器可以通过另一种types的指针来访问的问题。 在C89原理中给出的例子中,混叠被认为是显然不是任何联合的一部分的全局variables和指向不同types的指针之间,代码中的任何内容都不会表明会出现混叠。
一种解释严重削弱了语言,另一种解释则将某些优化的使用限制在“不符合”模式。 如果那些没有给予二等地位的优先考虑的人写下了C89来明确地符合他们的解释,那么标准的这些部分就会被广泛地谴责,并且会有某种明确的认识C的方言,它会尊重给定的规则的非破坏性的解释。
不幸的是,发生的事情是因为规则显然不要求编译器编写人员应用一个蹩脚的解释,大多数编译器编写者多年来以一种保留使C语言对系统编程有用的语义的方式来简单地解释规则; 程序员没有任何理由抱怨说,标准并没有强制编译器的行为是合理的,因为从他们的angular度来看,尽pipe标准太过松散,大家也应该这样做。 同时,有些人坚持认为,由于标准一直允许编译器处理Ritchie系统编程语言的语义弱化子集,所以没有理由要求符合标准的编译器处理其他任何事情。
这个问题的明智的解决scheme是认识到C被用于充分变化的目的,以至于应该有多种编译模式 – 一种所需的模式将把所有访问地址的东西视为直接读取和写入底层存储,并且与那些期望任何级别的基于指针的types双击支持的代码兼容。 另一种模式可能比C11更具限制性,除非代码明确地使用指令来指示已经被用作一种types的存储器何时和何处需要被重新解释或被再循环以作为另一种使用。 其他模式将允许一些优化,但支持一些将在更严格的方言下破解的代码; 没有对特定方言的特定支持的编译器可以用具有更多定义的别名行为替代。