C++ Standard Library ABI 是什么 ?

1,178 阅读6分钟

1. 问题背景

使用ubuntu安装LLVM libc++过程时,注意到LLVM官网对于libc++的简介:

The libc++ and libc++ ABI projects provide a standard conformant and high-performance implementation of the C++ Standard Library, including full support for C++11 and C++14.

疑惑libc++ ABI是什么 ?查看LLVM官网描述libc++ ABI:

libc++abi is a new implementation of low level support for a standard C++ library.

既然libc++abi是c++的底层支持,为什么要单独以so的形式出现 ?为什么不直接编译到libc++.so中呢 ?

2. 分析思路

2.1 ABI是什么 ?它的作用是什么 ?

ABI的全称是Application Binary Interface,如果仅是从字面意思来看,很难直接理解它的含义与功能,但是工作学习中大家更多接触的是API(Application Programming Interface,应用程序编程接口),那么二者之间有没有什么关系呢 ?接下来将从API出发,走向ABI。

C++的头文件,扮演着API的角色,开发人员使用其中定义的方法,完成对外部库的调用,所以API是直接对开发人员可见的,开发人员可以看得见摸得着的。但是ABI却不一样,从定义上看,它涉及到Binary,既然是二进制,显然开发人员不可能肉眼直接查看二进制,以获得有价值的信息,事实也是如此,ABI对开发人员不直接可见,它由硬件,编译器,链接器等因素影响。

举一个简单的例子,C++的Name Mangling标准,假设A项目需要依赖外部库third_party.so,该库定义并实现函数B,函数B在该库中的符号名称是_B2023_0401_, 如果A项目开发人员调用函数B,使用A项目中的c++编译器生成函数B的符号是_B2023_0405_,那么,程序链接时,将出现符号找不到的异常场景。究其原因,是因为A项目与外部库使用的编译器没有遵循相同的符号修饰标准,导致二进制不兼容。符号修饰标准仅仅是ABI的一部分,以此类推。

ABI的定义如下:

符号修饰标准,变量内存布局,函数调用方式等这些与可执行代码二进制兼容性相关的内容称为ABI

ABI与API都是应用程序接口,只是他们所描述的接口所在的层面不一样。API往往是指源代码级别的接口,而ABI是指二进制层面的接口

综上所述,API使用正确由开发人员保证,而ABI则由编译器,链接器等保证。一个在上,一个在下。ABI的作用也显而易见,就是保证二进制的兼容性。

2.2 C++ Standard Library ABI是什么 ?

C++ Standard Library ABI is the compilation of a given library API by a given compiler ABI.

library API + compiler ABI = library ABI

C++标准库ABI由C++标准库API和编译器ABI两部分构成,标准库API保证c++标准中对象(比如std:string, std::map等)底层实现的一致性,防止因底层实现不同导致的兼容性问题,compiler ABI则保证外部符号修饰,如何调用虚函数等而二进制兼容性。所以,如果需要实现二进制兼容性的完全体,需要同时满足library API和compiler ABI两个条件。那么便存在一个常见的问题,使用不同的C++标准库so,是否可以同时运行在一个进程空间内呢?操作系统平台是否对该使用场景存在限制呢 ?

2.3 LLVM的libc++ABI.so 与 C++ ABI之间的关系是什么呢 ?

C++ ABI是C++二进制兼容标准,标准的实现一部分通过编译器完成,比如如何调用虚函数等内容,而标准的另一部分实现,比如RTTI如何实现,则是位于libc++ABI.so, 因为这部分内容是运行时决定的。所以LLVM的libc++ABI.so是C++ ABI实现的子集,是进程运行时的一部分。

Ubuntu查看进程的maps信息(cat /proc/进程号/maps),可验证libc++ABI.so是进程运行的一部分。

截图 2023-04-06 00-30-36.png

如果查看进程maps信息,Andorid平台是否也能查看到libc++abi.so呢 ?

3. 答疑

3.1 使用不同的C++标准库so,是否可以同时运行在一个进程空间内呢 ?

问题分为两种情况,一种情况是C++ABI标准相同,答案是可以运行在同一进程空间,但存在限制,限制是不能在二者之间传递C++标准库对象(比如std::map等),因为标准库不同,可能std::map的实现也不同,存在对同一地址空间的不同解释,这可能会导致兼容性问题;第二种情况则是C++ABI标准不同,此毋庸置疑,答案肯定是不行的,程序链接时可能会因符号修饰标准不同而导致失败。

3.2 操作系统平台是否对混用不同C++标准库so的使用场景有限制呢 ?

Android平台存在限制,假设使用Android NDK编译的so,调用Android中位于/system/lib64/的系统so, 结果是可以调用,但是存在限制,不能传递C++标准库对象,因为程序链接时便会因无法解析的符号而导致链接失败。原因分析如下:

std::map头文件实现:

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

_LIBCPP_BEGIN_NAMESPACE_STD

template <class _Key, class _CP, class _Compare,
          bool = is_empty<_Compare>::value && !__libcpp_is_final<_Compare>::value>
class __map_value_compare
    : private _Compare
{

_LIBCPP_BEGIN_NAMESPACE_STD宏定义了命名空间,该宏定义在文件__config中。

AOSP中源码如下:

// Inline namespaces are available in Clang/GCC/MSVC regardless of C++ dialect.
#define _LIBCPP_BEGIN_NAMESPACE_STD namespace std { inline namespace _LIBCPP_ABI_NAMESPACE {
#define _LIBCPP_END_NAMESPACE_STD  } }

#ifndef _LIBCPP_ABI_NAMESPACE
# define _LIBCPP_ABI_NAMESPACE _LIBCPP_CONCAT(__,_LIBCPP_ABI_VERSION)
#endif

Android NDK中源码如下:

// Inline namespaces are available in Clang/GCC/MSVC regardless of C++ dialect.
#define _LIBCPP_BEGIN_NAMESPACE_STD namespace std { inline namespace _LIBCPP_ABI_NAMESPACE {
#define _LIBCPP_END_NAMESPACE_STD  } }

#ifndef _LIBCPP_ABI_NAMESPACE
# define _LIBCPP_ABI_NAMESPACE _LIBCPP_CONCAT(__ndk,_LIBCPP_ABI_VERSION)
#endif

请注意观察,_LIBCPP_ABI_NAMESPACE的定义是不一样的,也就是说虽然都是std::map,但是他们的命名空间确实不一样的,所以才会导致无法解析的符号。之前问题定位时,只知道命名空间不一样的,但是不知道为什么不一样 ?命名空间不一样,是为避免二进制兼容性问题。

3.3 如果查看进程maps信息,Andorid平台是否也能查看到libc++abi.so ?

答案是否定的,查看进程maps信息,可以看到有libc++.so,但是看不到libc++abi.so。通过查看libcxx的Adnroid.bp文件,原因一目了然,因为AOSP将libc++abi编译成静态库,集成到libc++.so。

4.参考

  1. 程序员的自我修养-链接,装载与库 章节4.4.3:C++与ABI

  2. GCC ABI Policy and Guidelines

  3. LLVM官网

  4. LLVM libcxxabi 项目

  5. AOSP源码,分支:android-13.0.0_r35

    map: external/libcxx/include/map

    __config: external/libcxx/include/__config

    Android.bp: external/libcxx/Android.bp

  6. Android NDK,版本:r25c

    __config: sources/cxx-stl/llvm-libc++/include/__config