zoukankan      html  css  js  c++  java
  • strict aliasing

    概述

    C/C++ 中的变量占有一块内存,这时这个变量就是这块内存的别名,指针也可以指向内存,因此同一块内存可能会有多个别名。

    int main()
    {
        int i = 0;
        int *ip = &i;
    }
    

    其中iip是同一块内存,都是它的别名。

    内存别名的存在会影响编译器生成的代码的行为。

    考虑以下代码块(来自 CSAPP 5.1 节):

    void twiddle1(long *xp, long *yp)
    {
        *xp += *yp;
        *xp += yp;
    }
    
    void tiwddle2(long *xp, long *yp)
    {
        *xp *= 2 * *yp;
    }
    

    这两个函数的功能看起来是相同的,但其实不然。加入,xpyp指向同一块内存,twiddle1()*xp写为原来的四倍,而twiddle2()xp写为原来的两倍。

    编译器在进行优化时,要确保优化是安全的,即优化的程序和未优化的程序行为是一致的。在上面的例子中,编译器无法判断xpyp是同一块内存的别名(指向同一块内存),只能保守地认为两个指针指向同一块内存,因此twiddle()要老老实实的进行两次+=

    而当指针指向的对象类型不同时,编译器可以放心地认为指针指向不同的内存,互相不为别名,这就是所谓的 strict aliasing不同类型的指针指向不同的内存块。在这种情况下,编译器可以使用激进的优化策略。

    但是 C/C++ 经常使用类型转换和指针直接操作内存,有时就会破坏 strict aliasing 规则,导致未定义行为

    #include <stdio.h>
    int global = 2;
    int test_strict_aliasing(int *arg)
    {
        global = 1;
        *reinterpret_cast<float*>(arg) = 0;
        return global;
    }
    
    int 
    main()
    {
        printf("%d
    ", test_strict_aliasing(&global));
        printf("global: %d
    ", global);
        return 0;
    }
    
    ----------------------------------------------------------------------
    g++ -Wall -Wstrict-aliasing=1 -o strict-aliasing strict-aliasing.cpp
    ----------------------------------------------------------------------
    0
    global: 0
        
    ------------------------------------------------------------------------
    g++ -Wall -Wstrict-aliasing=1 -O2 -o strict-aliasing strict-aliasing.cpp
    ------------------------------------------------------------------------
    0
    global: 1
    

    在上面的程序中reinterpret_cast<float *>(arg)创建了一个临时的float *指针,并且指向的内存块和int *类型指针arg相同,这是非法的内存别名,这反了 strict aliasing 规则,产生未定义行为。在 GCC O2 以下的优化级别,不假设 strict aliasing, 在 O2 及以上优化级别假设 strict aliasing,因此两编译选项下程序的行为不同。

    g++ -Wall -Wstrict-aliasing=1 -O2 -o strict-aliasing strict-aliasing.cpp 生成的汇编代码
      401170:	c7 05 aa 2e 00 00 01 	movl   $0x1,0x2eaa(%rip)        # 404024 <global>
      401177:	00 00 00 
      40117a:	b8 01 00 00 00       	mov    $0x1,%eax
      40117f:	c7 07 00 00 00 00    	movl   $0x0,(%rdi)
      401185:	c3                   	retq   
      401186:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
      40118d:	00 00 00 
    
    g++ -Wall -Wstrict-aliasing=1 -o strict-aliasing strict-aliasing.cpp 生成的汇编代码
      401126:	55                   	push   %rbp
      401127:	48 89 e5             	mov    %rsp,%rbp
      40112a:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
      40112e:	c7 05 ec 2e 00 00 01 	movl   $0x1,0x2eec(%rip)        # 404024 <global>
      401135:	00 00 00 
      401138:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
      40113c:	66 0f ef c0          	pxor   %xmm0,%xmm0
      401140:	f3 0f 11 00          	movss  %xmm0,(%rax)
      401144:	8b 05 da 2e 00 00    	mov    0x2eda(%rip),%eax        # 404024 <global>
      40114a:	5d                   	pop    %rbp
      40114b:	c3                   	retq   
    
    
    

    可以发现 O2 编译选项下生成的代码更少,性能更强。O2 下的代码直接将数字1当成返回值返回,而不是将global当成返回值返回,因此程序出现了错误的行为。

    将指针转型为不相容(imcompatible的指针类型,并进行读写违反了 strict aliasing,是严重的未定义行为。如果确实需要进行 type punning,必须将指针转换为相容的指针类型,即通过合法的内存别名访问内存。C/C++ 标准规定了以下类型的指针是合法的别名:

    1. 指针指向的类型相差unsignedsignedvolatile
    2. char *void *是所有指针的合法别名
    3. 指向包含指针指向对象类型的聚合类或 union 的指针是合法别名

    使用char *创建合法别名

    char在 C/C++ 中实际上是字节类型,使用非常频繁,因此在标准中为它开了“后门”。

    以下程序避免了未定义行为:

    int test_strict_aliasing(int *arg)
    {
        global = 1;
        *reinterpret_cast<float*>(arg) = 0;
        *reinterpret_cast<char *>(arg) = 0;
        *(reinterpret_cast<char *>(arg) + 1) = 0;
        *(reinterpret_cast<char *>(arg) + 2) = 0;
        *(reinterpret_cast<char *>(arg) + 3) = 0;
        return global;
    }
    

    使用union * 创建合法别名

    union int2float
    {
        int i;
        float f;
    };
    int test_strict_aliasing(int *arg)
    {
        global = 1;
        *reinterpret_cast<float*>(arg) = 0;
        return global;
    }
    
    

    上面的程序定义了一个包含我们要修改的指针指向的对象类型(int)的联合体,然后将arg转型为union int2float *再通过 union 修改*arg。这种方法是 GCC 推荐的方法。

    禁止编译器假设 strict aliasing

    上面提到,在 O2 及以上优化等级才会 假设 strict aliasing ,有大量的 C/C++ 程序必须违反 strict aliasing,因此 GCC 提供了-fstrict-aliasing-fno-strict-aliasing选项开启和关闭 strict aliasing。

    GCC 还提供了-Wstrict-aliasing来警告违反 strict-aliaisng 的行为,这个选项被-Wall默认开启。GCC 虽然提供了警告选项,但该功能工作的并不好。

    总结

    GCC 对 strict aliasing 的处理让很多 C/C++ 程序不加上-no-strict-aliasing选项就无法正确运行,这引起了很多人的愤怒,Linus 还专门喷过 GCC,但是经过我通过实验发现,Clang 也和 GCC 一样烂。

    总之,要避免直接对指针进行转型并读写,这是未定义的!!!

  • 相关阅读:
    C/C++的区别
    stm32之UCOS-III
    PID控制及整定算法
    PCB设计基础及技巧
    电路的一些基本理论
    stm32与三菱PLC通信
    stm32之外设控制
    stm32之内部功能
    JavaScript数组方法详解
    git新建关联克隆仓库指令
  • 原文地址:https://www.cnblogs.com/kongj/p/14031789.html
Copyright © 2011-2022 走看看