GCC编译宏_GLIBCXX_USE_CXX11_ABI背景分析和实现原理
Posted ithiker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 <string>
#include <iostream>
int main()
std::string s1("Hello, World.\\n");
std::string s2 = s1;
printf("%p\\n", s1.data());
printf("%p\\n", s2.data());
在GCC5.1前的编译器上输出s1和s2地址是一样的,测试代码。
从GCC5.1开始,输出的地址则不是一样的,测试代码。
GCC5.1之前的Copy–On-Write行为有什么不好呢,在这里不做详细分析,只看一个简单的例子:
#include <string>
#include <iostream>
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
上面的代码在GCC5.1之前的编译器上输出是什么呢,因为p是悬空指针,所以这里会产生了undefined behavior,比如这里的测试代码,会输出helloworld
为什么会这样呢,是因为GCC5.1之前std::string
实现Copy–On-Write行为是采用了和智能指针类似的引用计数方法。p
最开始指向string s
,s
赋值给s2
,此时s2
和s
指向同一块内存,但是因为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<int>, std::string的大小(测试代码)
std::sizeof(std::list<int>) | std::sizeof(std::string) | |
---|---|---|
GCC4.8 | 16 | 8 |
GCC7.5 | 24 | 32 |
实际上,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
在链接第三方库时,根据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 <iostream>
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;
参考:
- https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html
- https://docs.alliancecan.ca/wiki/GCC_C%2B%2B_Dual_ABI
- http://litaotju.github.io/c++/2019/02/24/Why-we-need-D_GLIBCXX_USE_CXX11_ABI=0/
- https://developers.redhat.com/blog/2015/02/05/gcc5-and-the-c11-abi
- https://blog.csdn.net/xiexievv/article/details/117381343
以上是关于GCC编译宏_GLIBCXX_USE_CXX11_ABI背景分析和实现原理的主要内容,如果未能解决你的问题,请参考以下文章
GCC编译宏_GLIBCXX_USE_CXX11_ABI背景分析和实现原理
将程序链接到库,每个库使用不同的 _GLIBCXX_USE_CXX11_ABI