• GCC编译宏_GLIBCXX_USE_CXX11_ABI背景分析和实现原理



    背景

    最近项目中涉及到使用两个版本的GCC编译得到的第三方库, 由于部分比较久远的第三方库是使用GCC4.8编译的,而新版的第三方库是使用GCC7.3编译的。项目本身是使用GCC7.3编译的, 如果在在项目中如果要混用这两个版本的第三方库,则必须在项目编译的时候,就加上编译宏-D_GLIBCXX_USE_CXX11_ABI=0

    本文将介绍为什么需要_GLIBCXX_USE_CXX11_ABI以及其实现原理。


    一、使用_GLIBCXX_USE_CXX11_ABI的背景

    在GCC说明文档中,需要_GLIBCXX_USE_CXX11_ABI是因为GCC的ABI变化,为什么要引入这个变化呢,主要是为了支持新的C++标准中要禁止std::string的Copy-On-Write行为和支持std::list中size()的时间复杂度为O(1)。

    In the GCC 5.1 release libstdc++ introduced a new library ABI that includes new implementations of std::string and std::list. These changes were necessary to conform to the 2011 C++ standard which forbids Copy-On-Write strings and requires lists to keep track of their size.

    1. std::string的Copy-On-Write行为

    下面的代码:

    #include 
    #include 
    
    int main() {
        std::string s1("Hello, World.\n");
        std::string s2 = s1;
        printf("%p\n", s1.data());
        printf("%p\n", s2.data());
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在GCC5.1前的编译器上输出s1和s2地址是一样的,测试代码
    从GCC5.1开始,输出的地址则不是一样的,测试代码
    GCC5.1之前的Copy–On-Write行为有什么不好呢,在这里不做详细分析,只看一个简单的例子:

    #include 
    #include 
    
    
    
    int main() {
       std::string s("str");
       const char* p = s.data();
       {
            std::string s2(s);
            (void) s[0];
        }
        
        std::string a("helloworld");
        std::cout << p << std::endl;  // p is dangling
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    上面的代码在GCC5.1之前的编译器上输出是什么呢,因为p是悬空指针,所以这里会产生了undefined behavior,比如这里的测试代码,会输出helloworld

    为什么会这样呢,是因为GCC5.1之前std::string实现Copy–On-Write行为是采用了和智能指针类似的引用计数方法。p最开始指向string s,s赋值给s2,此时s2s指向同一块内存,但是因为s[0]需要写s对应的内存,由于Copy–On-Write机制,这时,s2指向s的内存,当s2析构后,s2指向的内存也同时析构,p指向的内存也同时析构。

    在GCC5.1和5.1之后的编译器,只要使用默认的-D_GLIBCXX_USE_CXX11_ABI=1,那么,上面的代码就是可以正常运行的。

    2. list::size()的时间复杂度

    这篇C++ std::list中size()的时间复杂度中分析了list在GCC5.1之前和之后的时间复杂度,也和_GLIBCXX_USE_CXX11_ABI 关系密切。

    3. 使用_GLIBCXX_USE_CXX11_ABI的原因

    从上面1和2的分析,GCC5.1之后,要实现上面的功能,必须修改std::string和std::list的源代码,对于std::list, 它必须添加一个成员记录当前list的元素个数才能保证std::list时间复杂度为O(1)的要求。
    我们看下GCC5.1前后std::list, std::string的大小(测试代码

    std::sizeof(std::list)std::sizeof(std::string)
    GCC4.8168
    GCC7.52432

    实际上,GCC5.1上面通过添加_GLIBCXX_USE_CXX11_ABI宏,使同一份代码中同时支持两份ABI。
    那么,为什么一定要通过宏的方式,使新版的GCC同时支持新旧ABI呢?这是因为,如果通过改变soname的方式,当同一个程序dlopen两个不同版本的shared library时,必然会发生命名冲突。所以,通过使用编译宏_GLIBCXX_USE_CXX11_ABI,其值要么是0,要么是1,一个单独编译的项目中任意时候都只有一份实现。

    二、_GLIBCXX_USE_CXX11_ABI的实现原理

    如果当前项目需要链接第三方库的API,比如第三方库编译时使用_GLIBCXX_USE_CXX11_ABI=0,当本项目编译时用的是_GLIBCXX_USE_CXX11_ABI=1,这个时候两个库ABI不兼容,应该报错; 当本项目编译时也用的是_GLIBCXX_USE_CXX11_ABI=0,这个时候两个库ABI是一样的,应该编译通过。

    第三方库(_GLIBCXX_USE_CXX11_ABI=0)第三方库(_GLIBCXX_USE_CXX11_ABI=1)
    本项目(_GLIBCXX_USE_CXX11_ABI=0)编译通过编译不通过
    本项目(_GLIBCXX_USE_CXX11_ABI=1)编译不通过编译通过

    所以,这里有两个问题:

    • 当链接第三方库时GCC是如何区别不同的ABI版本?
    • 当GCC采用不同的编译宏,项目源代码不变的情况下,项目源代码为何可以编译通过?

    这里,第一个问题是借助name mangling的方式解决的,第二个问题是通过inline namespace解决的。

    1. name mangling

    假设我们的代码中有这样的三个函数:void blargle() and void blargle(int) and void argle::blargle(), 那么编译器会通过name mangling机制为每个函数生成唯一的函数名供外部程序使用或链接使用,上面的函数产生的内部函数名如下:

    _Z7blarglev
    _Z7blarglei
    _ZN5argle7blargleEv
    
    • 1
    • 2
    • 3

    在链接第三方库时,根据name mangling机制提供的唯一的函数名,可以做到只有相同版本的ABI才能链接成功。

    2. inline namespace

    inline namespace是C++11推出的一个新特性,它的介绍如下:

    An inline namespace is a namespace that uses the optional keyword inline in its original-namespace-definition.
    Members of an inline namespace are treated as if they are members of the enclosing namespace in many situations (listed below). This property is transitive: if a namespace N contains an inline namespace M, which in turn contains an inline namespace O, then the members of O can be used as though they were members of M or N.

    也就是说,对于次低层namespace, 如果在其前面加上inline, 那么外部可以直接忽略这个。

    所以,结合name mangling和inline namespace,宏_GLIBCXX_USE_CXX11_ABI的原理类似等同于下面代码中的宏xxx。当定义了宏xxx=1时,程序输出"hello, world from cxx11",反之输出"hello, world from std"

    #include 
    
    namespace std {
    #if xxx 
    inline namespace  cxx11 {
       void printHello() {
           std::cout << "hello, world from cxx11" <<std::endl;
       }
    }
    #else
    
       void printHello() {
           std::cout << "hello, world from std" <<std::endl;
       }
    #endif
    
    }
    
    int main() {
       std::printHello();
    
       return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    参考:

    1. https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html
    2. https://docs.alliancecan.ca/wiki/GCC_C%2B%2B_Dual_ABI
    3. http://litaotju.github.io/c++/2019/02/24/Why-we-need-D_GLIBCXX_USE_CXX11_ABI=0/
    4. https://developers.redhat.com/blog/2015/02/05/gcc5-and-the-c11-abi
    5. https://blog.csdn.net/xiexievv/article/details/117381343
  • 相关阅读:
    关于 打开虚拟机出现“...由VMware产品创建,但该产品与此版VMwareWorkstateion不兼容,因此无法使用” 的解决方法
    windows系统中用windbg收集程序崩溃信息
    Linux每日智囊
    Leetcode6247-从链表中移除节点
    电影《乌云背后的幸福线》观后感
    python批量读取nc气象数据并转为tif
    田字描红贴
    【node】如何在打包前进行请求等操作npm run build
    常用 时间类型的相互转化
    Unreal地形高级材质之根据斜率分配材质
  • 原文地址:https://blog.csdn.net/gigglesun/article/details/126447778