c++标准库abi兼容性问题
今天在编一个比较老的工程,编译链接是正常通了,但是在启动时报找不到符号:
undefined symbol: _ZNK6google8protobuf7Message11GetTypeNameB5cxx11Ev
c++filt查看一下符号名,这是protobuf的一个函数:
$ c++filt _ZNK6google8protobuf7Message11GetTypeNameB5cxx11Ev
google::protobuf::Message::GetTypeName[abi:cxx11]() const
$ rgrep GetTypeName
protobuf/message.h: virtual string GetTypeName() const;
再看一下工程自带的protobuf库文件,发现这符号是在的,只不过不带[abi:cxx11]:
$ nm -C libprotobuf.a |grep GetTypeName
U google::protobuf::Message::GetTypeName() const
U google::protobuf::Message::GetTypeName() const
00000000000001f0 T google::protobuf::Message::GetTypeName() const
符号存在也理所当然,不然这工程也不可能正常运行这么久。
那问题就在这个[abi:cxx11]了,以下是我搜索了一圈之后的一些个人理解。
abi
abi全称是Application Binary Interface,译为程序二进制接口,表示程序在二进制层面上的规范。
compiler abi
编译器的编译规范可以算是一种abi。
典型的就是name mangling,如果大家都按自己的标准来,那链接时根本找不到对应的符号。
例如std::cout在libstdc++.so.6中被译作_ZSt4cout,假如你的代码使用了std::cout,而编译出来的目标文件把它译作了其他不一样的符号,那链接时必然会报Undefined symbols。
一般来说同一编译器相近版本abi兼容性没什么问题,但版本差异过大就不好说了,例如:
Visual Studio 版本之间的 C++ 二进制兼容性。
library abi
另一种情况是库的abi问题。
对c++来说,类是定义在头文件里的。假设一个libFoo.so,里面暴露了一个类Foo,旧版本只有一个int成员a,但在新版本又加了一个int成员b。
假设库的使用者没有更新Foo.h重新编译,就直接使用新的Foo动态库。
可以想象,在新动态库里会使用到新的成员b,但这个b根本就没人给它分配过内存,使用者创建时认为Foo依然只有一个int的小,自然就是灾难。
也就是说程序与库对Foo的大小在二进制层面上认知不一致,也算是一种abi问题吧。
abi:cxx11
对于libstdc++来说,在改版时也遇到了abi兼容性问题,其中一之就是改了std::string的实现(至于改了什么我就不关注了)。
而从上面可以看到GetTypeName的返回值正是一个string,旧的libprotobuf.a还在用旧的std::string,但现在已经是新时代了。
另一方面,也幸好gcc给我们用新的符号来表示当前是依赖于cxx11的实现,不然连符号都不改的话,那程序一旦运行起来就自求多福了。
同时,gcc也提供了返回旧时代的门票,在编译时定义-D_GLIBCXX_USE_CXX11_ABI=0,指定使用旧版本的实现。
how it works?
看源码:
#if _GLIBCXX_USE_CXX11_ABI
// 把新版本放到std::__cxx11命名空间内,也就是换了新符号
_GLIBCXX_BEGIN_NAMESPACE_CXX11
template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
// 新版本定义
};
_GLIBCXX_END_NAMESPACE_CXX11
#else // !_GLIBCXX_USE_CXX11_ABI
template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
// 旧版本定义
};
#endif // !_GLIBCXX_USE_CXX11_ABI
libstdc++.so.6里同时包含了两个实现:
00000000000f4070 W std::string::append(unsigned long, char)
0000000000145580 W std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(unsigned long, char)
last question
最后一个问题,为什么我遇到的情况是在程序启动的时候才报错?通常不是编译链接时就会报错吗?
如果是编译可执行文件,那链接时的确要求每一个符号都有定义,无论是在自己身上或是在依赖的动态库上都可以。理由也很简单,可执行文件在编译时可以列出自己的所有依赖,换言之所有符号的定义也只能存在于这些依赖中。如果找完了所有地方还是没找到符号的定义,那就真没定义了。
但如果是编译动态库,那就没有这个要求,毕竟它还不知道会被谁使用,或许使用者会提供这个符号的实现呢。
而我上面的代码是恰恰是编成了一个python用的模块,所以编译链接时并没有报错。然后python在import这个模块时触发dlopen,这时就发现找不到符号的定义了。