gcc、严格混叠和通过联合进行强制转换
Posted
技术标签:
【中文标题】gcc、严格混叠和通过联合进行强制转换【英文标题】:gcc, strict-aliasing, and casting through a union 【发布时间】:2011-02-23 18:47:49 【问题描述】:你有什么恐怖故事要讲吗? GCC 手册最近添加了一条关于 -fstrict-aliasing 和通过联合强制转换指针的警告:
[...] 获取地址、转换结果指针并取消引用结果具有未定义的行为 [强调添加],即使转换使用联合类型,例如:
union a_union
int i;
double d;
;
int f()
double d = 3.0;
return ((union a_union *)&d)->i;
有没有人可以举例说明这种未定义的行为?
请注意,这个问题不是关于 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;
工作正常,使用 CYGWIN:
-2147483648
-2147483648
查看汇编程序,我们看到 gcc 完全优化了t
away:f1()
只是存储了预先计算的答案:
movl $-2147483648, %eax
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)
而且这些函数也是内联的(这似乎是一些微妙的严格混叠错误的原因),但这与这里无关。 (而且这个汇编器没有那么重要,但它增加了确凿的细节。)
另请注意,获取地址显然是错误的(或者正确,如果您试图说明未定义的行为)。例如,正如我们知道这是错误的:
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
我们同样知道这是错误的:
extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior
有关这方面的背景讨论,请参见示例:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdfhttp://gcc.gnu.org/ml/gcc/2010-01/msg00013.htmlhttp://davmac.wordpress.com/2010/02/26/c99-revisited/http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html ( = search page on Google 然后查看缓存页面)
What is the strict aliasing rule?C99 strict aliasing rules in C++ (GCC)
在第一个链接中,七个月前的 ISO 会议纪要草稿,一位与会者在第 4.16 节中指出:
有没有人认为规则足够清晰?没有人能够真正理解它们。
其他说明:我的测试是使用 gcc 4.3.4,使用 -O2;选项 -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
。
【问题讨论】:
你编译的优化级别是什么?优化级别越高,编译器就越有可能依赖严格的别名规则。 (顺便说一句,委员会会议记录中的引用可能适用于 ISO 标准的许多部分:-P) 小点:你可能应该使用int64_t
来确保联合中的整数元素与double
的大小相同。
你可以看看这个例子:***.com/questions/1812348/a-question-about-union-in-c/…
请注意,工会可能比其每个成员有更强的对齐要求。
John Regehr 给出了两个有趣、简短的 GCC 和 Clang 不一致示例。
【参考方案1】:
GCC 警告工会这一事实并不一定意味着工会目前不起作用。但这里有一个比你的稍微简单的例子:
#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);
输出:
$ 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
所以在 GCC 4.3.4 上,联合“拯救了一天”(假设我想要输出“-3”)。它禁用依赖于严格混叠的优化,并在第二种情况下(仅)导致输出“3”。使用 -Wall,USE_UNION 还会禁用类型双关语警告。
我没有要测试的 gcc 4.4,但请试一试这段代码。您的代码实际上测试了d
的内存是否在通过联合读回之前被初始化:我的代码测试它是否被修改。
顺便说一句,将 double 的一半读取为 int 的安全方法是:
double d = 3;
int i;
memcpy(&i, &d, sizeof i);
return i;
在 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
所以没有实际调用 memcpy。如果您不这样做,那么如果联合强制转换在 GCC 中停止工作,您应该得到什么 ;-)
【讨论】:
问题的关键在于 GCC 手册规定工会不会总是“挽救局面”。但是每个测试似乎都显示,就像你的一样,它们实际上运行良好。当然,不包括在 cellperformance 链接中标记为 INVALID 的示例。 手册(至少,您引用的那一点)指出代码具有未定义的行为。这并不排除工会禁用严格的别名假设。因此,工会可能总是“挽救局面”,而 GCC 包含额外的警告只是为了教育用户,并为他们未来的一些变化做好准备。这可能会或可能不会发生。我猜 GCC 优化方面的专家可以根据第一原理构造一个失败案例,如果有的话,否则就是随机搜索。 同意! 我不是gcc方面的专家,这就是我问SO的原因。 @SteveJessop 那么是否建议使用 memcpy 而不是联合或隐式强制转换?【参考方案2】:好吧,这有点像死灵贴,但这是一个恐怖故事。我正在移植一个假设本机字节顺序是大端的程序。现在我也需要它来处理小端。不幸的是,我不能在任何地方都使用本机字节顺序,因为可以通过多种方式访问数据。例如,一个 64 位整数可以被视为两个 32 位整数或 4 个 16 位整数,甚至可以被视为 16 个 4 位整数。更糟糕的是,没有办法弄清楚内存中究竟存储了什么,因为该软件是某种字节码的解释器,而数据是由该字节码形成的。例如,字节码可能包含写入 16 位整数数组的指令,然后以 32 位浮点数的形式访问它们中的一对。并且无法预测或更改字节码。
因此,我必须创建一组包装类来处理以大端顺序存储的值,而不管本机字节顺序如何。在 Linux 上的 Visual Studio 和 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;
此代码用于将存储在 32 位整数中的浮点数的 32 位表示转换为双精度。似乎编译器决定在赋值给 D 之后对 *pF 进行赋值——结果是第一次执行代码时,D 的值是垃圾,结果值“延迟”了 1 次迭代。
奇迹般地,当时没有其他问题。所以我决定继续并在原始平台上测试我的新代码,HP-UX 在具有原生大端顺序的 RISC 处理器上。现在它又坏了,这次是在我的新班级里:
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;
;
由于一些非常奇怪的原因,第一个赋值运算符只正确地分配了字节 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;
但它也没有工作。事实上,它稍微好一点——它只对负数失败。
此时我正在考虑为本地字节序添加一个简单的测试,然后通过char*
指针和if (LITTLE_ENDIAN)
进行所有检查。更糟糕的是,该程序大量使用了周围的联合,目前这似乎可以正常工作,但在所有这些混乱之后,如果它无缘无故突然中断,我不会感到惊讶。
【讨论】:
@user2472093,然后它会在其他编译器上咬我吗?不,谢谢。事实上,我想我已经拥有了。关于在发布配置中破坏 MSVC 的一些事情。最糟糕的是,它仅在数百个中的一个特定值上崩溃,而该特定值只有一点错误。但是那个位在顺序部分,所以结果完全不同。 @user2472093:任何体面的编译器都应该有一个等效于“-fno-strict-aliasing”的选项;记录使用此类选项的要求可能比依赖编译器遵守任何特定的别名规则更安全。具有讽刺意味的是,编译器的积极性使得程序员有必要主动阻止基于别名的优化形式,这不会造成麻烦[无论是通过编译器选项,还是通过到处使用 memcpy],但是似乎是事态。 @supercat,但更安全的是摆脱不正确的别名并让编译器优化所有那些丑陋的char*
s。这就是我最终所做的,从那时起我不记得这个特定软件有任何麻烦。
@SergeyTachenov:不是。由于使用char*
绕过更激进的别名限制的代码通常无法优化为与符合旧别名要求的直接代码一样高效,因此一些编译器已不再将“char*”视为真正的“指向任何东西”的指针努力恢复效率。由于不知道未来的编译器会在这方面做什么,我想说在明确且明确定义的“-fno-strict-aliasing”规则下编写最有效的代码会更简单[可能没有正式的它的名字,但是......【参考方案3】:
您断言以下代码是“错误的”:
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
... 错了。仅获取两个联合成员的地址并将它们传递给外部函数不会导致未定义的行为;您只能通过以无效方式取消引用其中一个指针来获得它。例如,如果函数 foo 立即返回而不取消引用您传递给它的指针,那么该行为不是未定义的。通过严格阅读 C99 标准,甚至在某些情况下指针 可以在不调用未定义行为的情况下被取消引用;例如,它可以读取第二个指针引用的值,然后通过第一个指针存储一个值,只要它们都指向动态分配的对象(即没有“声明类型”的对象)。
【讨论】:
@davmac--你是对的。我需要实际定义一个示例函数。也许void f(int *i, double *d)*i = 1; *d = 2;
?这两个语句可以通过严格别名以任意顺序执行。但是(我猜)如果将__attribute__((may_alias))
添加到参数中,语句将按原样执行。
@Joseph,严格别名不允许 允许以任一顺序执行存储,因为允许存储更改对象的有效类型 (C99 6.5p7) .它确实允许针对不允许别名的存储对读取进行重新排序。更好的示例是int f(int *i, double *d) *i = 1; *d = 2; return *i
啊,抱歉,在这种情况下,我写的内容不太有意义,因为我们正在讨论具有声明类型的联合对象。但是,基本上您是先存储到一个工会成员,然后再存储到另一个;我认为这仍然是标准允许的(即使你是通过指针而不是通过联合类型来实现的)。尽管不可否认,如果您对它进行深入研究,该标准就会开始变得毫无意义。最近的 C99 修正案(我认为是 TC 3)似乎允许存储到一个联合成员,然后读取另一个,但该值未指定。
C99 的技术勘误 3 N1265 添加脚注 82:'如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,值的对象表示的适当部分被重新解释为新类型中的对象表示,如 6.2.6 中所述(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。'
而且我相信f
是“错误的”,使用 TC3 6.5.2.3 示例 3 中的话,“因为联合类型不是 可见 在函数 f" 内。【参考方案4】:
当编译器有两个指向同一块内存的不同指针时,就会发生混叠。通过对指针进行类型转换,您将生成一个新的临时指针。例如,如果优化器对汇编指令重新排序,访问这两个指针可能会产生两个完全不同的结果——它可能会在写入相同地址之前重新排序读取。这就是为什么它是未定义的行为。
你不太可能在非常简单的测试代码中看到问题,但是当有很多事情发生时它就会出现。
我认为警告是要明确联合不是特殊情况,即使您可能期望它们是。
有关别名的更多信息,请参阅此 Wikipedia 文章:http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization
【讨论】:
我愿意接受一个复杂的问题示例,在任何常用的编译器上。 我认为术语“别名”通常承认两个引用之间的别名发生在特定上下文中,如果 (1) 两个引用都在该上下文中使用,并且 (2) 两个引用都不是明显新鲜派生的在使用时从另一个。即使像someAggregate.intMember = 23;
这样的东西在标准下也将是UB,但不应被视为“别名”,因为在导出左值someAggregate.intMember
和最后一次使用它之间,将执行存储上的所有操作使用后一个左值。
如果有人接受关于 6.5p7 的目的是告诉编译器何时必须识别别名的脚注,这表明质量编译器不应在不涉及实际别名的情况下不必要地限制语言,这将定义高质量的编译器应该如何处理someAggregate.intmember = 23;
,消除对有效类型规则的需求,并且对于程序员和编译器编写者来说会更好。【参考方案5】:
你见过这个吗? What is the strict aliasing rule?
该链接包含指向本文的辅助链接,其中包含 gcc 示例。 http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
尝试这样的联合会更接近问题。
union a_union
int i;
double *d;
;
这样你就有两种类型,一个 int 和一个 double* 指向同一个内存。在这种情况下,使用双精度 (*(double*)&i)
可能会导致问题。
【讨论】:
1) 该问题两次引用了 Mike Acton 在 cellperformace 链接中的内容丰富且内容丰富的文章。但是请注意,其他链接之一不同意他的观点。 2) Paul R 已经注意到在现实世界中sizeof(double)
通常大于sizeof(int)
。但这在这里无关紧要,无论如何,这个例子都来自 GCC 手册。【参考方案6】:
这是我的:认为这是所有 GCC v5.x 及更高版本中的错误
#include <iostream>
#include <complex>
#include <pmmintrin.h>
template <class Scalar_type, class Vector_type>
class simd
public:
typedef Vector_type vector_type;
typedef Scalar_type scalar_type;
typedef union conv_t_union
Vector_type v;
Scalar_type s[sizeof(Vector_type) / sizeof(Scalar_type)];
conv_t_union();
conv_t;
static inline constexpr int Nsimd(void)
return sizeof(Vector_type) / sizeof(Scalar_type);
Vector_type v;
template <class functor>
friend inline simd SimdApply(const functor &func, const simd &v)
simd ret;
simd::conv_t conv;
conv.v = v.v;
for (int i = 0; i < simd::Nsimd(); i++)
conv.s[i] = func(conv.s[i]);
ret.v = conv.v;
return ret;
;
template <class scalar>
struct RealFunctor
scalar operator()(const scalar &a) const
return std::real(a);
;
template <class S, class V>
inline simd<S, V> real(const simd<S, V> &r)
return SimdApply(RealFunctor<S>(), r);
typedef simd<std::complex<double>, __m128d> vcomplexd;
int main(int argc, char **argv)
vcomplexd a,b;
a.v=_mm_set_pd(2.0,1.0);
b = real(a);
vcomplexd::conv_t conv;
conv.v = b.v;
for(int i=0;i<vcomplexd::Nsimd();i++)
std::cout << conv.s[i]<<" ";
std::cout << std::endl;
应该给
c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11
c010200:~ peterboyle$ ./a.out
(1,0)
但在 -O3 下:我认为这是错误的并且是编译器错误
c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3
c010200:~ peterboyle$ ./a.out
(0,0)
g++4.9下
c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3
c010200:~ peterboyle$ ./a.out
(1,0)
llvm xcode下
c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3
c010200:~ peterboyle$ ./a.out
(1,0)
【讨论】:
我认为您的代码避免了 UB(在 GNU C++ 中,其中定义了联合类型双关语,就像在 ISO C99/C11 中一样,但不是 ISO C++。请注意,这是C 问题)。无论如何,看起来这个错误在 gcc6.3 中已修复,但仍然存在 gcc6.2:godbolt.org/g/M2mpSr。请注意,gcc6.3 编译它时使用.LC0:
保存 FP 常量,但 gcc6.2 使用 vxorpd
在寄存器中创建 0.0
。有一个警告:51: ignoring attributes on template argument '__m128d aka __vector(2) double'
(vcomplexd
的定义),但不知道是不是意味着“向量”类型只是double
...【参考方案7】:
我真的不明白你的问题。编译器完全按照您的示例应该做的事情。 union
转换是您在 f1
中所做的。在f2
中,它是一个普通的指针类型转换,你将它转换为一个联合是无关紧要的,它仍然是一个指针casting
【讨论】:
2) 指向 AndreyT 答案的链接似乎暗示 gcc 是正确的,而世界其他地方是错误的。但这不是问题。我正在寻找恐怖故事。甚至是一个小例子。 好的。您是否跟进了 davmac.wordpress.com/2010/02/26/c99-revisited 博客条目?尤其是davmac.wordpress.com/2009/10/25/…,他在 mysql 中发现了一个别名错误。这可能就是您正在寻找的。span> @tristopia:我的问题非常狭窄,关于通过指针的联合双关语。我刚刚在***.com/questions/2958633/… 中问了一个更一般的问题。以上是关于gcc、严格混叠和通过联合进行强制转换的主要内容,如果未能解决你的问题,请参考以下文章