2018年11月08日

GCC5以降のlibstdc++のデフォルトABI変更について

GCC 5 以降の C++ ライブラリ、libstdc++ はデフォルトの ABI が変更されているため、それ以前の g++ でコンパイルしたバイナリとは(デフォルトでは)リンク互換性がありません。ただし、旧 ABI もサポートしているので、Dual ABI サポートのライブラリとなっています。

これについてまとまった日本語情報が見当たらなかったのでまとめてみます。



確認環境には、GCC 7 と GCC 4.8 が簡単に共存できるので、Ubutun 18.04 を使用します。apt-get で g++ と g++-4.8 をインストールしてあるものとします。
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.1 LTS (Bionic Beaver)"
...
$ sudo apt-get install g++ g++-4.8
...
$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.3.0-27ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)

$ g++-4.8 -v
Using built-in specs.
COLLECT_GCC=g++-4.8
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.5-4ubuntu8' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libmudflap --enable-plugin --with-system-zlib --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.8.5 (Ubuntu 4.8.5-4ubuntu8)
注意する点として、GCC 7 の方は --enable-default-pie 付きで configure されているので、デフォルトで -fPIC が付いたコードを生成します。そのため、libstdc++ とは無関係の部分でデフォルトでのリンク互換性が失われています。今回は -fPIC オプションを両方に付けることで、あらかじめこの差異を埋めておきます。

以下が確認プログラムとなります。
$ cat str.cpp
#include <string>

std::string myStr = "hello, world!";

$ cat test.cpp
#include <iostream>
#include <string>

extern std::string myStr;

int main()
{
    std::cout << myStr << std::endl;
    return 0;
}
まず、普通にそれぞれコンパイルした場合には、当然何の問題もありません。
$ g++-4.8 -Wall -fPIC -c str.cpp -o str48.o
$ g++-4.8 -Wall -fPIC -c test.cpp -o test48.o
$ g++-4.8 str48.o test48.o
$ ./a.out
hello, world!
$ g++ -Wall -fPIC -c str.cpp -o str7.o
$ g++ -Wall -fPIC -c test.cpp -o test7.o
$ g++ str7.o test7.o
$ ./a.out
hello, world!
しかし、4.8 でコンパイルした o ファイルと 7 でコンパイルした o ファイルをリンクしようとすると、エラーになります。
$ g++ str48.o test7.o
test7.o: 関数 `main' 内:
test.cpp:(.text+0x7): `myStr[abi:cxx11]' に対する定義されていない参照です
collect2: error: ld returned 1 exit status
エラーメッセージの中で [abi:cxx11] という部分が目を引きます。これが変更された ABI となります。

nm でそれぞれのバイナリを見て見ると、シンボルが変わっています。
$ nm str48.o
...
0000000000000000 B myStr
$ nm str7.o
...
0000000000000000 B _Z5myStrB5cxx11
...
シンボルをデマングルしてみると、上記エラーメッセージと同様になります。
$ c++filt _Z5myStrB5cxx11
myStr[abi:cxx11]
追記:コメント欄で教えていただきましたが、nm -C でデマングル済みのシンボルを表示できます。
$ nm -C str7.o
...
0000000000000000 B myStr[abi:cxx11]
これだけなら、単にコンパイラとライブラリのメジャーバージョンが上がってるのだから、ライブラリの互換性無くなっても仕方ないですよね、という話で終わりなのですが、libstdc++ のすごい所は、旧 ABI もサポートしている Dual ABI ということです。

The GNU C++ Library Manual / 3. Using / Dual ABI

以下のように C++11 ABI を使用するというマクロ _GLIBCXX_USE_CXX11_ABI を 0 に定義すると、4.8 のバイナリとリンクできてちゃんと動作します。
$ g++ -Wall -fPIC -D_GLIBCXX_USE_CXX11_ABI=0 -c test.cpp -o test7_old.o
$ g++ str48.o test7_old.o
$ ./a.out
hello, world!
この C++11 ABI というのは、C++11 の仕様というわけでも、GCC の C++ ABI 自体が変わったわけでもありません。GCC の C++ コンパイラの生成コードの ABI は GCC 3 からずっと Itanium C++ ABI という仕様で、不変です。

この ABI 変更は、libstdc++ がライブラリレベルで行っているもので、C++11 のインライン名前空間という機能と GCC の attribute による独自 ABI 定義機能を使用して実現しています。(おそらくこれが理由で C++11 ABI という名前になっているのでしょうか?)

追記: C++11 の仕様変更が、この ABI 変更の理由なので、C++11 ABI という名前のようです。C++11 からマルチスレッドが仕様に含まれるようになり、パフォーマンスの低下を防ぐために std::string(basic_string)を Copy On Write (リファレンスカウント方式)で実装することが禁止されたため、ABI を変えざるを得なくなったようです。

C++11 の機能を使っているのだから、それ以前の std を指定した場合は使えないのでは?と思いますが、例えば C++98 を指定しても使えてしまってますね…。

実際の実装は、以下のような感じになってます。一切 #if __cplusplus >= 201103L のような C++11 の場合分けがありませんので、この inline namespace 機能は GCC の場合は常に有効なのでしょうか。これはちょっと謎ですが、後述する ABI tag を付けた inline namespace は、tagging inline namespace という特別扱いなので、C++11 の仕様では無い、みたいにも読めます。
$ vim /usr/include/x86_64-linux-gnu/c++/7.3.0/bits/c++config.h
...
# define _GLIBCXX_USE_DUAL_ABI 1

#if ! _GLIBCXX_USE_DUAL_ABI
// Ignore any pre-defined value of _GLIBCXX_USE_CXX11_ABI
# undef _GLIBCXX_USE_CXX11_ABI
#endif

#ifndef _GLIBCXX_USE_CXX11_ABI
# define _GLIBCXX_USE_CXX11_ABI 1
#endif

#if _GLIBCXX_USE_CXX11_ABI
namespace std
{
  inline namespace __cxx11 __attribute__((__abi_tag__ ("cxx11"))) { }
}
namespace __gnu_cxx
{
  inline namespace __cxx11 __attribute__((__abi_tag__ ("cxx11"))) { }
}
# define _GLIBCXX_NAMESPACE_CXX11 __cxx11::
# define _GLIBCXX_BEGIN_NAMESPACE_CXX11 namespace __cxx11 {
# define _GLIBCXX_END_NAMESPACE_CXX11 }
# define _GLIBCXX_DEFAULT_ABI_TAG _GLIBCXX_ABI_TAG_CXX11
#else
# define _GLIBCXX_NAMESPACE_CXX11
# define _GLIBCXX_BEGIN_NAMESPACE_CXX11
# define _GLIBCXX_END_NAMESPACE_CXX11
# define _GLIBCXX_DEFAULT_ABI_TAG
#endif
ABI が変更されたクラス、関数、変数は _GLIBCXX_(BEGIN | END)_NAMESPACE_CXX11 マクロで囲まれ、インライン名前空間内で cxx11 というタグが付けられます。インライン名前空間は名前空間修飾無しでアクセスできますが、当然名前空間が異なるので、ソースレベルの互換性は保たれますが、バイナリ互換性は無くなります。

これだけでも、動作が変わってしまったクラス等があやまってダイナミックリンクされてしまい動作がおかしくなる、といういわゆる DLL ヘルを防ぐことはできますが、単に名前空間が異なるというだけだと、ユーザが名前空間の指定を間違った場合と区別が付かず、なぜかリンクできない???と悩んでしまう恐れがあります。(また、名前空間だけでは、クラスのサイズの変更や戻り値の型の変更等がマングル名に反映されないという問題点もあります。)

そこで ABI、具体的にはシンボルのマングルルール自体を変えてしまう attribute __abi_tag__ を使用して、C++11 ABI だからリンクできないということをわかりやすくしているのでしょう。(ドキュメントによると、inline namespace に abi_tag を付けた場合は特別扱いで、厳密にはマングルルールは変わらず、単にタグを付けるだけと書いてますが、まあ一定のルールでシンボルの名前が変更されるわけですから、実質同じに思えます。)

_GLIBCXX_USE_CXX11_ABI が 0 の場合は何もせず、互換性があるコードを残しておけば、同じライブラリコードを _GLIBCXX_USE_CXX11_ABI が 1 と 0 で 2 回コンパイルするだけで、Dual ABI をサポートすることが可能になるというカラクリです。

Clang も 3.9 から abi_tag をサポートしているので、GCC の libstdc++ を使用することができます。(このドキュメントに書かれている GCC ABI version 10 とは何なのでしょうか?)

あと警告を出しているのは Binutils なのですが、とりあえず手元で試した所、2.20 の c++filt は abi:cxx11 をデコードできませんでしたが、2.25 はできるので、少なくとも 2.25 からのサポートのようです。GCC 5.1 のリリースが 2015/4 で、Binutils 2.25 のリリースが 2015/1 とタイトなので、おそらく 2.24 ぐらいからサポートされてるような気もします。

kmckk at 12:06コメント(2)GCC | 若槻 

コメント一覧

1. Posted by koba   2018年11月10日 16:34
nm で -C オプションをつけるとデマングルされたシンボル名を表示してくれるので便利です。
2. Posted by 若槻   2018年11月19日 15:32
コメントありがとうございます。nm -C の存在忘れてました(笑)

コメントする

名前
 
  絵文字
 
 
記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード