gcc,严格别名,并通过联合进行转换

时间:2010-05-25 16:06:42

标签: c gcc type-conversion unions strict-aliasing

你有什么恐怖故事可讲吗? GCC手册最近添加了一个关于-fstrict-aliasing和通过union转换指针的警告:

  

[...]获取地址,强制生成指针并取消引用结果 未定义行为 [强调添加],即使转换使用了联合类型,例如:

    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 完全优化tf1()只是存储预先计算的答案:

movl    $-2147483648, %eax

while 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.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
(= 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; options -O2和-O3暗示-fstrict-aliasing。 GCC手册中的示例假设sizeof(double)&gt; = sizeof(int);它们是不平等无所谓。

另外,正如Mike Acton在cellperformace链接中所指出的那样-Wstrict-aliasing=2,但 =3,此处为此示例生成warning: dereferencing type-punned pointer might break strict-aliasing rules

7 个答案:

答案 0 :(得分:11)

海湾合作委员会警告关于工会的事实并非必然意味着工会目前不起作用。但这是一个比你的简单的例子:

#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的内存是否在通过联合回读之前被初始化:我的测试是否被修改。

顺便说一句,读取一半的双重作为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停止工作,你应该得到的东西; - )

答案 1 :(得分:4)

您断言以下代码是“错误的”:

extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior

......错了。只取两个联合成员的地址并将它们传递给外部函数不会导致未定义的行为;你只能通过以无效方式取消引用其中一个指针来获得它。例如,如果函数foo立即返回而没有解除引用它传递的指针,那么行为不是未定义的。通过严格阅读C99标准,甚至有些情况下指针可以取消引用而不调用未定义的行为;例如,它可以读取第二个指针引用的值,然后通过第一个指针存储一个值,只要它们都指向动态分配的对象(即没有“声明类型”的对象)。

答案 2 :(得分:3)

当编译器有两个指向同一块内存的不同指针时,会发生别名。通过类型转换指针,您将生成一个新的临时指针。例如,如果优化器重新排序汇编指令,则访问两个指针可能会产生两个完全不同的结果 - 它可能会在写入同一地址之前重新排序读取。这就是为什么它是未定义的行为。

你不太可能在非常简单的测试代码中看到问题,但是当有很多事情发生时它会出现。

我认为警告是要明确工会不是一个特例,即使你可能期望它们。

有关别名的更多信息,请参阅此维基百科文章:http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

答案 3 :(得分:3)

嗯,这是一个坏死的帖子,但这是一个恐怖的故事。我正在移植一个程序,该程序是在假设本机字节顺序是大端的情况下编写的。现在我也需要它来处理小端。不幸的是,我无法在任何地方使用本机字节顺序,因为可以通过多种方式访问​​数据。例如,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; 

此代码用于将存储在32位整数中的float的32位表示形式转换为double。似乎编译器在分配给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;
        }
};

出于一些非常奇怪的原因,第一个赋值运算符只正确地分配了字节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)进行检查。更糟糕的是,该计划大量使用工会,现在似乎工作正常,但是如果它突然中断,我不会感到惊讶。

答案 4 :(得分:2)

你看到了这个吗? 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;
};

这样你有2种类型,一个int和一个double *指向同一个内存。在这种情况下,使用双(*(double*)&i)可能会导致问题。

答案 5 :(得分:1)

这是我的:认为这是所有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) 

答案 6 :(得分:0)

我真的不明白你的问题。编译器完全按照它在您的示例中所做的操作。 union转化是您在f1中所做的转化。在f2中,它是一个普通的指针类型转换,你将它转换为联合是无关紧要的,它仍然是一个指针强制转换