首页 文章

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

提问于
浏览
33

你有任何恐怖故事要讲吗? 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 是永远不会直接读取的临时变量,并且永远不会通过'somewhat-compatible'指针读取,所以编译器可能无需设置它 . 然后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

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 Session 的 Session 记录草稿,一位与会者在4.16节中注意到:

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

其他说明:我的测试是使用gcc 4.3.4,含有-O2; options -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 .

7 回答

  • 2

    海湾合作委员会对工会发出警告这一事实并不一定意味着工会不比你的工会简单得多:

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

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

  • 11

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

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

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

  • 0

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

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

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

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

  • 4

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

    但它也没有用 . 事实上,它有点好 - 它只对负数而言失败了 .

    此时我正在考虑为本机字节序添加一个简单的测试,然后通过 if (LITTLE_ENDIAN) 指针执行所有操作,并使用 if (LITTLE_ENDIAN) 检查 . 更糟糕的是,该计划大量使用工会,现在似乎工作正常,但在这一切混乱之后,如果它突然间没有明显的原因,我不会感到惊讶 .

  • 1

    你见过这个吗 ? 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 (*(double*)&i) 可能会导致问题 .

  • 3

    Here is mine: 认为这是所有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;
    }
    

    Should give

    c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 
    c010200:~ peterboyle$ ./a.out 
    (1,0)
    

    But under -O3: I THINK THIS IS WRONG AND A COMPILER ERROR

    c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3 
    c010200:~ peterboyle$ ./a.out 
    (0,0)
    

    Under g++4.9

    c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3 
    c010200:~ peterboyle$ ./a.out 
    (1,0)
    

    Under llvm xcode

    c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3 
    c010200:~ peterboyle$ ./a.out 
    (1,0)
    
  • 3

    我真的不明白你的问题 . 编译器完全按照它在您的示例中所做的操作 . union 转换就是您在 f1 中所做的 . 在 f2 它's a normal pointer typecast, that you casted it to a union is irrelevant, it'仍然是一个指针 casting

相关问题