38

你有什么恐怖故事要讲吗?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了: f1()简单地存储预先计算的答案:

movl    $-2147483648, %eax

whilef2()将 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(=
在 Google 上搜索页面然后查看缓存页面)

什么是严格的别名规则?
C++ (GCC) 中的 C99 严格别名规则

在第一个链接中,七个月前 ISO 会议的会议记录草稿,一位参与者在第 4.16 节中指出:

有没有人认为规则足够清晰?没有人能够真正解释它们。

其他说明:我的测试是使用 gcc 4.3.4,使用 -O2;选项 -O2 和 -O3 暗示 -fstrict-aliasing。GCC 手册中的示例假定 sizeof(double) >= sizeof(int); 他们是否不平等并不重要。

此外,正如 Mike Acton 在 cellperformace 链接中所指出的,这里的示例生成了-Wstrict-aliasing=2,但不是。 =3warning: dereferencing type-punned pointer might break strict-aliasing rules

4

7 回答 7

13

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 中停止工作,您将应得的 ;-)

于 2010-06-02T15:17:01.343 回答
5

好吧,这有点像死灵贴,但这是一个恐怖故事。我正在移植一个假设本机字节顺序是大端的程序。现在我也需要它来处理小端。不幸的是,我不能在任何地方都使用本机字节顺序,因为可以通过多种方式访问​​数据。例如,一个 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)进行检查。更糟糕的是,该程序大量使用了周围的联合,目前这似乎工作正常,但在所有这些混乱之后,如果它突然无缘无故地中断,我不会感到惊讶。

于 2011-10-27T12:23:34.180 回答
4

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

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

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

于 2010-07-11T13:25:00.777 回答
3

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

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

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

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

于 2010-05-25T16:33:41.073 回答
2

Have you seen this ? What is the strict aliasing rule?

The link contains a secondary link to this article with gcc examples. http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Trying a union like this would be closer to the problem.

union a_union {
    int i;
    double *d;
};

That way you have 2 types, an int and a double* pointing to the same memory. In this case using the double (*(double*)&i) could cause the problem.

于 2010-06-01T16:04:32.417 回答
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) 
于 2017-05-06T13:15:40.190 回答
0

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

于 2010-06-01T17:12:52.950 回答