Jos*_*sey 34 c gcc type-conversion strict-aliasing unions
你有任何恐怖故事要讲吗?GCC手册最近添加了一个关于-fstrict-aliasing的警告并通过联合转换指针:
[...]获取地址,强制生成指针并取消引用结果具有未定义的行为 [强调添加],即使转换使用了联合类型,例如:
union a_union {
int i;
double d;
};
int f() {
double d = 3.0;
return ((union a_union *)&d)->i;
}
Run Code Online (Sandbox Code Playgroud)
有没有人有一个例子来说明这种未定义的行为?
请注意,这个问题不是关于C99标准所说或不说的.它是关于gcc和其他现有编译器的实际功能.
我只是猜测,但一个潜在的问题可能在于设置d为3.0.因为d是永远不会直接读取的临时变量,并且永远不会通过"稍微兼容"的指针读取,所以编译器可能不会费心去设置它.然后f()将从堆栈中返回一些垃圾.
我的简单,天真,尝试失败了.例如:
#include <stdio.h>
union a_union {
int i;
double d;
};
int f1(void) {
union a_union t;
t.d = 3333333.0;
return t.i; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3)
}
int f2(void) {
double d = 3333333.0;
return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior'
}
int main(void) {
printf("%d\n", f1());
printf("%d\n", f2());
return 0;
}
Run Code Online (Sandbox Code Playgroud)
工作正常,给CYGWIN:
-2147483648
-2147483648
Run Code Online (Sandbox Code Playgroud)
看看汇编程序,我们看到gcc完全优化t了: f1()只需存储预先计算的答案:
movl $-2147483648, %eax
Run Code Online (Sandbox Code Playgroud)
同时f2()将3333333.0推送到浮点堆栈,然后提取返回值:
flds LC0 # LC0: 1246458708 (= 3333333.0) (--> 80 bits)
fstpl -8(%ebp) # save in d (64 bits)
movl -8(%ebp), %eax # return value (32 bits)
Run Code Online (Sandbox Code Playgroud)
并且函数也是内联的(这似乎是一些微妙的严格别名错误的原因),但这与此无关.(而这个汇编程序并不相关,但它增加了确凿的细节.)
另请注意,获取地址显然是错误的(或者正确,如果您试图说明未定义的行为).例如,就像我们知道这是错误的:
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
Run Code Online (Sandbox Code Playgroud)
我们同样知道这是错的:
extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior
Run Code Online (Sandbox Code Playgroud)
有关此问题的背景讨论,请参阅示例:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http:// davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
(= 在谷歌搜索页面然后查看缓存页面)
什么是严格别名规则?
C++中的C99严格别名规则(GCC)
在第一个链接中,七个月前ISO会议的会议记录草稿,一位与会者在4.16节中注意到:
有没有人认为规则足够清楚?没有人真正能够解释它们.
其他说明:我的测试是使用gcc 4.3.4,含有-O2; options -O2和-O3暗示-fstrict-aliasing.GCC手册中的示例假设sizeof(double)> = sizeof(int); 它们是不平等无所谓.
此外,正如Mike Acton在cellperformace链接中所指出的那样-Wstrict-aliasing=2,但不是 =3,warning: dereferencing type-punned pointer might break strict-aliasing rules这里为此示例生成.
Ste*_*sop 12
海湾合作委员会警告工会的事实并不一定意味着工会目前没有工作.但这是一个比你的简单的例子:
#include <stdio.h>
struct B {
int i1;
int i2;
};
union A {
struct B b;
double d;
};
int main() {
double d = 3.0;
#ifdef USE_UNION
((union A*)&d)->b.i2 += 0x80000000;
#else
((int*)&d)[1] += 0x80000000;
#endif
printf("%g\n", d);
}
Run Code Online (Sandbox Code Playgroud)
输出:
$ gcc --version
gcc (GCC) 4.3.4 20090804 (release) 1
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc -oalias alias.c -O1 -std=c99 && ./alias
-3
$ gcc -oalias alias.c -O3 -std=c99 && ./alias
3
$ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias
-3
$ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias
-3
Run Code Online (Sandbox Code Playgroud)
所以在GCC 4.3.4上,联盟"保存了一天"(假设我想要输出"-3").它禁用依赖于严格别名的优化,并在第二种情况下(仅)导致输出"3".使用-Wall,USE_UNION也会禁用类型双关语警告.
我没有gcc 4.4来测试,但请给这个代码一个去.你的代码实际上测试了d在通过union回读之前是否初始化了内存:我们测试它是否被修改.
顺便说一句,读取double的一半作为int的安全方法是:
double d = 3;
int i;
memcpy(&i, &d, sizeof i);
return i;
Run Code Online (Sandbox Code Playgroud)
通过GCC优化,可以得到:
int thing() {
401130: 55 push %ebp
401131: 89 e5 mov %esp,%ebp
401133: 83 ec 10 sub $0x10,%esp
double d = 3;
401136: d9 05 a8 20 40 00 flds 0x4020a8
40113c: dd 5d f0 fstpl -0x10(%ebp)
int i;
memcpy(&i, &d, sizeof i);
40113f: 8b 45 f0 mov -0x10(%ebp),%eax
return i;
}
401142: c9 leave
401143: c3 ret
Run Code Online (Sandbox Code Playgroud)
所以没有实际调用memcpy.如果你不是这样做的话,如果工会在GCC停止工作,你应该得到你所得到的;-)
嗯,这有点像死尸贴,但这是一个恐怖故事。我正在移植一个假设本机字节顺序是大端的程序。现在我也需要它来处理小端。不幸的是,我不能在任何地方都使用本机字节顺序,因为可以通过多种方式访问数据。例如,一个 64 位整数可以被视为两个 32 位整数或 4 个 16 位整数,甚至是 16 个 4 位整数。更糟糕的是,没有办法弄清楚内存中究竟存储了什么,因为该软件是某种字节码的解释器,而数据是由该字节码形成的。例如,字节码可能包含写入 16 位整数数组的指令,然后将其中的一对作为 32 位浮点数访问。并且没有办法预测它或改变字节码。
因此,我必须创建一组包装类来处理以大端顺序存储的值,而不管本机端序如何。在 Visual Studio 和 Linux 上的 GCC 中完美运行,无需优化。但是使用 gcc -O2 后,地狱就崩溃了。经过大量调试,我发现原因在这里:
double D;
float F;
Ul *pF=(Ul*)&F; // Ul is unsigned long
*pF=pop0->lu.r(); // r() returns Ul
D=(double)F;
Run Code Online (Sandbox Code Playgroud)
此代码用于将存储在 32 位整数中的浮点数的 32 位表示转换为双精度数。似乎编译器决定在赋值给 D 之后对 *pF 进行赋值——结果是第一次执行代码时,D 的值是垃圾,随后的值“延迟”了 1 次迭代。
神奇的是,当时没有其他问题。所以我决定继续在原始平台上测试我的新代码,在具有原生大端顺序的 RISC 处理器上的 HP-UX。现在它又坏了,这次是在我的新班级:
typedef unsigned long long Ur; // 64-bit uint
typedef unsigned char Uc;
class BEDoubleRef {
double *p;
public:
inline BEDoubleRef(double *p): p(p) {}
inline operator double() {
Uc *pu = reinterpret_cast<Uc*>(p);
Ur n = (pu[7] & 0xFFULL) | ((pu[6] & 0xFFULL) << 8)
| ((pu[5] & 0xFFULL) << 16) | ((pu[4] & 0xFFULL) << 24)
| ((pu[3] & 0xFFULL) << 32) | ((pu[2] & 0xFFULL) << 40)
| ((pu[1] & 0xFFULL) << 48) | ((pu[0] & 0xFFULL) << 56);
return *reinterpret_cast<double*>(&n);
}
inline BEDoubleRef &operator=(const double &d) {
Uc *pc = reinterpret_cast<Uc*>(p);
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
return *this;
}
inline BEDoubleRef &operator=(const BEDoubleRef &d) {
*p = *d.p;
return *this;
}
};
Run Code Online (Sandbox Code Playgroud)
出于一些非常奇怪的原因,第一个赋值运算符只正确分配了字节 1 到 7。字节 0 总是有一些废话,这破坏了一切,因为有一个符号位和一部分顺序。
我尝试使用联合作为解决方法:
union {
double d;
Uc c[8];
} un;
Uc *pc = un.c;
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
*p = un.d;
Run Code Online (Sandbox Code Playgroud)
但它也不起作用。事实上,它要好一点 - 它只对负数失败。
在这一点上,我正在考虑为本机字节序添加一个简单的测试,然后通过char*带有if (LITTLE_ENDIAN)检查的指针来做所有事情。更糟糕的是,该程序大量使用了工会,目前这似乎还可以,但毕竟这些混乱,如果它突然无缘无故中断,我不会感到惊讶。
当编译器有两个不同的指针指向同一块内存时,就会发生别名。通过对指针进行类型转换,您将生成一个新的临时指针。例如,如果优化器重新排序汇编指令,则访问两个指针可能会给出两个完全不同的结果 - 它可能会在写入同一地址之前重新排序读取。这就是为什么它是未定义的行为。
您不太可能在非常简单的测试代码中看到这个问题,但是当发生很多事情时它就会出现。
我认为这个警告是为了明确工会并不是一个特例,尽管你可能认为它们是特例。
有关别名的更多信息,请参阅此维基百科文章:http://en.wikipedia.org/wiki/Aliasing_( computing)#Conflicts_with_optimization