gcc、严格别名和通过联合强制转换

gcc、严格别名和通过联合强制转换,c,gcc,type-conversion,unions,strict-aliasing,C,Gcc,Type Conversion,Unions,Strict Aliasing,你有什么恐怖故事要讲吗?GCC手册最近添加了一个关于-fstrict别名和通过联合强制转换指针的警告: […]获取地址、强制转换结果指针并取消对结果的引用具有未定义的行为,即使强制转换使用联合类型,例如: 有没有人举个例子来说明这种未定义的行为 注意:这个问题不是关于C99标准说了什么或没有说什么。它是关于当前gcc和其他现有编译器的实际功能 我只是猜测,但一个潜在的问题可能在于将d设置为3.0。由于d是一个临时变量,它从不直接读取,也从不通过“某种程度上兼容”的指针读取,因此编译器可能不会费心

你有什么恐怖故事要讲吗?GCC手册最近添加了一个关于-fstrict别名和通过联合强制转换指针的警告:

[…]获取地址、强制转换结果指针并取消对结果的引用具有未定义的行为,即使强制转换使用联合类型,例如:

有没有人举个例子来说明这种未定义的行为

注意:这个问题不是关于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;
}
查看汇编程序,我们看到,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
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 *);
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
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
有关这方面的背景讨论,请参见示例:

#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;
}




(=然后查看缓存页面)


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

有没有人认为规则已经够清楚了?没有人真正能够解释它们

其他注释:我的测试是使用GCC4.3.4,使用-O2;选项-O2和-O3表示-fstrict别名。GCC手册中的示例假设sizeof(double)=sizeof(int);他们是否不平等并不重要


此外,正如Mike Acton在CellPerformance链接中指出的那样,
-Wstrict aliasing=2
,而不是
=3
,会产生
警告:在这里的示例中,取消引用类型双关指针可能会破坏严格的别名规则。

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

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

我认为这一警告是为了明确工会不是特例,尽管你可能会认为它们是特例

有关别名的更多信息,请参阅这篇维基百科文章:

您看到了吗?

该链接包含指向本文的第二个链接以及gcc示例

尝试这样一个联盟会更接近问题所在

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

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

我不太理解您的问题。编译器完全按照您的示例中的要求执行。
union
转换就是您在
f1
中所做的转换。在
f2
中,它是一个普通的指针类型转换,您将它转换到联合是不相关的,它仍然是一个指针转换

GCC警告联合的事实并不一定意味着联合当前不起作用。但这里有一个比你的略不简单的例子:

union a_union {
    int i;
    double *d;
};
#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);
}
因此,在GCC4.3.4中,工会“节省了时间”(假设我需要输出“-3”)。它禁用依赖严格混叠的优化,并在第二种情况下(仅)导致输出“3”。对于-Wall,USE_UNION还禁用类型pun警告

我没有GCC4.4要测试,但请尝试一下这段代码。您的代码实际上测试
d
的内存在通过联合读回之前是否已初始化:我的代码测试它是否已修改

顺便说一句,将双精度的一半读取为整数的安全方法是:

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工作;-)

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


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

这有点像necro帖子,但这里有一个可怕的故事。我正在移植一个程序,它是在假设本机字节顺序是big-endian的情况下编写的。现在我也需要它来处理小endian。不幸的是,我不能到处使用本机字节顺序,因为数据可以通过多种方式访问。例如,一个64位整数可以被视为两个
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;
#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) 
c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(0,0) 
c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0) 
c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0)