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

Jos*_*sey 34 c gcc type-conversion strict-aliasing unions

你有任何恐怖故事要讲吗?GCC手册最近添加了一个关于-fstrict-aliasing的警告并通过联合转换指针:

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

    union a_union {
        int i;
        double d;
    };

    int f() {
        double d = 3.0;
        return ((union a_union *)&d)->i;
    }
Run Code Online (Sandbox Code Playgroud)

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

请注意,这个问题不是关于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;
}
Run Code Online (Sandbox Code Playgroud)

工作正常,给CYGWIN:

-2147483648
-2147483648
Run Code Online (Sandbox Code Playgroud)

看看汇编程序,我们看到gcc完全优化t了: f1()只需存储预先计算的答案:

movl    $-2147483648, %eax
Run Code Online (Sandbox Code Playgroud)

同时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)
Run Code Online (Sandbox Code Playgroud)

并且函数也是内联的(这似乎是一些微妙的严格别名错误的原因),但这与此无关.(而这个汇编程序并不相关,但它增加了确凿的细节.)

另请注意,获取地址显然是错误的(或者正确,如果您试图说明未定义的行为).例如,就像我们知道这是错误的:

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

我们同样知道这是错的:

extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior
Run Code Online (Sandbox Code Playgroud)

有关此问题的背景讨论,请参阅示例:

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
(= 在谷歌搜索页面然后查看缓存页面)

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

在第一个链接中,七个月前ISO会议的会议记录草稿,一位与会者在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这里为此示例生成.

Ste*_*sop 12

海湾合作委员会警告工会的事实并不一定意味着工会目前没有工作.但这是一个比你的简单的例子:

#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);
}
Run Code Online (Sandbox Code Playgroud)

输出:

$ 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
Run Code Online (Sandbox Code Playgroud)

所以在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;
Run Code Online (Sandbox Code Playgroud)

通过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
Run Code Online (Sandbox Code Playgroud)

所以没有实际调用memcpy.如果你不是这样做的话,如果工会在GCC停止工作,你应该得到你所得到的;-)


Ser*_*nov 5

嗯,这有点像死尸贴,但这是一个恐怖故事。我正在移植一个假设本机字节顺序是大端的程序。现在我也需要它来处理小端。不幸的是,我不能在任何地方都使用本机字节顺序,因为可以通过多种方式访问​​数据。例如,一个 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; 
Run Code Online (Sandbox Code Playgroud)

此代码用于将存储在 32 位整数中的浮点数的 32 位表示转换为双精度数。似乎编译器决定在赋值给 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;
        }
};
Run Code Online (Sandbox Code Playgroud)

出于一些非常奇怪的原因,第一个赋值运算符只正确分配了字节 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;
Run Code Online (Sandbox Code Playgroud)

但它也不起作用。事实上,它要好一点 - 它只对负数失败。

在这一点上,我正在考虑为本机字节序添加一个简单的测试,然后通过char*带有if (LITTLE_ENDIAN)检查的指针来做所有事情。更糟糕的是,该程序大量使用了工会,目前这似乎还可以,但毕竟这些混乱,如果它突然无缘无故中断,我不会感到惊讶。


Mar*_*som 3

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

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

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

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