web-dev-qa-db-ja.com

gcc、厳密なエイリアシング、およびユニオンを介したキャスト

伝えるべきホラーストーリーはありますか? GCCマニュアルは最近、-fstrict-aliasingとユニオンを介したポインターのキャストに関する警告を追加しました。

[...]アドレスを取得し、結果のポインターをキャストし、結果を逆参照すると、未定義の動作が発生します[強調を追加]、キャストが共用体タイプを使用している場合でも、例:

    union a_union {
        int i;
        double d;
    };

    int f() {
        double d = 3.0;
        return ((union a_union *)&d)->i;
    }

この未定義の振る舞いを説明する例はありますか?

この質問は、C99標準が何を言っているかではないか、または言っていないことに注意してください。これは、gccおよびその他の既存のコンパイラーの実際の機能に関するものです。

推測しているだけですが、潜在的な問題の1つは、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

アセンブラを見ると、gcctを完全に最適化していることがわかります:f1()は事前に計算された回答を保存するだけです:

movl    $-2147483648, %eax

f2()は3333333.0をfloating-pointスタックにプッシュし、戻り値を抽出します。

flds   LC0                 # LC0: 1246458708 (= 3333333.0) (--> 80 bits)
fstpl  -8(%ebp)            # save in d (64 bits)
movl   -8(%ebp), %eax      # return value (32 bits)

また、関数もインライン化されています(これは、いくつかの微妙な厳密なエイリアシングのバグの原因のようです)が、ここでは関係ありません。 (そして、このアセンブラーはそれほど関連性がありませんが、裏付けとなる詳細を追加します。)

また、アドレスの取得が明らかに間違っていることにも注意してください(または、未定義の動作を説明しようとしている場合は、right)。たとえば、私たちが知っているように、これは間違っています。

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厳密なエイリアシングルール

7か月前のISO会議の議事録草案である最初のリンクで、ある参加者はセクション4.16に次のように述べています。

ルールが十分明確だと思う人はいますか?誰もそれらを実際に解釈することはできません。

その他の注意事項:私のテストはgcc 4.3.4、-O2でした。オプション-O2および-O3は、-fstrict-aliasingを意味します。 GCCマニュアルの例では、sizeof(double)> =sizeof(int);を想定しています。それらが等しくないかどうかは関係ありません。

また、cellperformaceリンクでMike Actonが指摘しているように、-Wstrict-aliasing=2ですが、not=3は、ここでの例のwarning: dereferencing type-punned pointer might break strict-aliasing rulesを生成します。

35
Joseph Quinsey

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」が必要だと仮定します)。厳密なエイリアシングに依存する最適化を無効にし、2番目のケース(のみ)で出力「3」を生成します。 -Wallを使用すると、USE_UNIONは型のパンニングの警告も無効にします。

テストするgcc4.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で機能しなくなった場合に得られるものに値します;-)

12
Steve Jessop

次のコードが「間違っている」というあなたの主張:

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

... 間違っている。 2つのユニオンメンバーのアドレスを取得して外部関数に渡すだけでは、未定義の動作は発生しません。これは、これらのポインターの1つを無効な方法で逆参照することによってのみ得られます。たとえば、関数fooが、渡したポインタを逆参照せずにすぐに戻る場合、動作は未定義ではありません。 C99標準を厳密に読むと、未定義の動作を呼び出さずにポインターcanが逆参照される場合もあります。たとえば、2番目のポインタによって参照される値を読み取り、動的に割り当てられたオブジェクト(つまり、「宣言された型」のないオブジェクト)を指している限り、最初のポインタを介して値を格納できます。

4
davmac

エイリアシングは、コンパイラが同じメモリへの2つの異なるポインタを持っている場合に発生します。ポインターを型キャストすることにより、新しい一時ポインターを生成します。たとえば、オプティマイザがアセンブリ命令を並べ替える場合、2つのポインタにアクセスすると、まったく異なる2つの結果が得られる可能性があります。つまり、同じアドレスへの書き込みの前に読み取りを並べ替える可能性があります。これが未定義の動作である理由です。

非常に単純なテストコードで問題が発生する可能性はほとんどありませんが、多くのことが行われているときに問題が発生します。

警告は、あなたが期待しているとしても、組合は特別な場合ではないことを明確にすることだと思います。

エイリアシングの詳細については、このウィキペディアの記事を参照してください。 http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

3
Mark Ransom

ちょっとネクロ投稿ですが、ここにホラーストーリーがあります。ネイティブバイトオーダーがビッグエンディアンであるという前提で作成されたプログラムを移植しています。今、私はそれがリトルエンディアンでも機能する必要があります。残念ながら、データにはさまざまな方法でアクセスできるため、どこでもネイティブバイトオーダーを使用することはできません。たとえば、64ビット整数は2つの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回の反復で「遅れ」ました。

奇跡的に、その時点で他の問題はありませんでした。そこで、元のプラットフォームである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;
_

しかし、それも機能しませんでした。実際、それは少し良かった-それは負の数でのみ失敗した。

この時点で、ネイティブエンディアンの簡単なテストを追加し、if (LITTLE_ENDIAN)チェックを使用して_char*_ポインターを介してすべてを実行することを考えています。さらに悪いことに、このプログラムはあちこちでユニオンを多用しているので、今のところうまくいくようですが、結局のところ、明らかな理由もなく突然壊れても驚かないでしょう。

3
Sergei Tachenov

これ見たことある ? 厳密なエイリアシングルールとは何ですか?

このリンクには、gccの例を含むこの記事へのセカンダリリンクが含まれています。 http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

このような組合を試みることは問題に近いでしょう。

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

そうすれば、同じメモリを指すintとdouble *の2つのタイプがあります。この場合、double (*(double*)&i)を使用すると問題が発生する可能性があります。

2
Cobusve

ここに私のものがあります:これはすべてのGCCv5.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の下で:私はこれを考えますIS間違っていてコンパイラエラー

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) 
1
Peter Boyle

私はあなたの問題を本当に理解していません。コンパイラーは、あなたの例で想定されていたことを正確に実行しました。 union変換は、f1で行ったことです。 f2では、これは通常のポインタータイプキャストであり、ユニオンにキャストしたことは関係ありませんが、それでもポインターですキャスト

0