玩式草子─ソフトウェアとたわむれる日々

第34回C++とGCCの-fvisibilityオプション

WindowsやMac OS Xのように、1つの企業が開発環境や標準ライブラリを管理して、必要とあれば全ての開発者に新しいツールやライブラリを強制できる商用OSに比べて、世界中に分散した開発者がボランティアとして開発に参加しているOSSの世界では、新しいツールやライブラリが広く利用されるには過去との互換性を保つことが重要になります。

そのため、GCCやGlibc、X Window Systemといった主要なソフトウェアでは、過去のバージョンを用いていたソフトウェアも動作するように、バージョンアップを繰り返して中身がそっくり変わってしまっても、APIやABIは過去のバージョンとの(後方)互換性を保つように努力しています。

そのおかげで、最近では、OSの基盤となるこれらソフトウェアを更新する際も、以前のように互換性についてあれこれ調査する必要が無くなりました。その一方で、README等のファイルを調べるのをサボりがちになったので、バージョンアップ時に採用された新機能等を見落してしまうことも多くなりました。今回は、そのような新機能がらみでハマった話題を紹介しましょう。

libQtに起因する謎のエラー

KDE回りを新しいバージョンに追従するために、KDEのベースとなっているQtを4.7.1から4.8.1に更新したところ、Qtを使うソフトウェアのビルドができなくなりました。

具体的には、最近のデスクトップ環境の基盤となっているDBus上にメニュー構造のデータを流すためのDBusmenuライブラリのQt版、libdbusmenu-qt-0.9.2をビルドしようとすると、リンク時に「参照しているシンボルが見つからない("undefined reference"⁠⁠」というエラーになってしまいました。

$ make
...
[ 51%] Built target dbusmenu-qt
Linking CXX executable dbusmenuexportertest
CMakeFiles/dbusmenuexportertest.dir/dbusmenuexportertest.cpp.o: 
     In function `DBusMenuExporterTest::testGetSomeProperties()':
dbusmenuexportertest.cpp:(.text+0x1df7): undefined reference to 
   `DBusMenuExporter::DBusMenuExporter(QString const&, QMenu*, 
     QDBusConnection const&)'
dbusmenuexportertest.cpp:(.text+0x275b): undefined reference to 
   `DBusMenuExporter::~DBusMenuExporter()'
dbusmenuexportertest.cpp:(.text+0x27df): undefined reference to 
   `DBusMenuExporter::~DBusMenuExporter()'

このエラーメッセージは、⁠dbusmenuexportertest.cpp.oというオブジェクトファイルを実行形式にするために他のライブラリとリンクしようとしたところ、リンクしようとしているどのライブラリにもDBusMenuExporter::DBusMenuExporter(QString...)等の関数が見つからないのでリンクできなかった」という意味です。

今までの経験では、この種のトラブルの原因は、必要なライブラリの指定がMakefileの中で抜けていたり、ライブラリのあるディレクトリが正しく指定されていないことでした。

そこでまずMakefileの内容をチェックしてみましたが、このソフトウェアはCMakeを利用しているためMakefileの書式が独特で、どの部分で問題のリンクを実行しているのかがよくわかりません。

ざっと眺めたところでは、CMakeで生成されたMakefileでは、今回知りたいコンパイルやリンク時のオプション指定等をMakefile自体ではなく、Makefileから参照するファイル群に記述しているようなので追いかけるのが面倒です。そこでmakeコマンドが実行している内容を詳細に表示させるためのVERBOSE=1というオプションを指定して、どのようなコマンドを実行しているのか表示させることにしました。

$ make VERBOSE=1
/usr/bin/cmake -H/nfs/Srcs/L/Libs/Dbusmenu-qt/libdbusmenu-qt-0.9.2 \
   -B/nfs/Srcs/L/Libs/Dbusmenu-qt/build --check-build \
   -system CMakeFiles/Makefile.cmake 0
usr/bin/cmake -E cmake_progress_start \
  /nfs/Srcs/L/Libs/Dbusmenu-qt/build/CMakeFiles \
  /nfs/Srcs/L/Libs/Dbusmenu-qt/build/CMakeFiles/progress.marks \
  make -f CMakeFiles/Makefile2 all
....
Linking CXX executable dbusmenuexportertest
cd /nfs/Srcs/L/Libs/Dbusmenu-qt/build/tests && \
  /usr/bin/cmake -E cmake_link_script \
  CMakeFiles/dbusmenuexportertest.dir/link.txt --verbose=1
/usr/bin/g++  -isystem /usr/include -m64 \
  CMakeFiles/dbusmenuexportertest.dir/dbusmenuexportertest.cpp.o \
  CMakeFiles/dbusmenuexportertest.dir/testutils.cpp.o  \
  -o dbusmenuexportertest -rdynamic -lQtGui -lQtCore -lQtDBus -lQtTest \
  ../src/libdbusmenu-qt.so.2.6.0 -lQtGui -lQtCore -lQtDBus \
  -Wl,-rpath,/nfs/Srcs/L/Libs/Dbusmenu-qt/build/src
CMakeFiles/dbusmenuexportertest.dir/dbusmenuexportertest.cpp.o: \
 In function `DBusMenuExporterTest::testGetSomeProperties()':
dbusmenuexportertest.cpp:(.text+0x1df7): undefined reference to \
 `DBusMenuExporter::DBusMenuExporter(QString const&, QMenu*, \
  QDBusConnection const&)'
...

この結果を見ると、リンク時のコマンドは

/usr/bin/g++  -isystem /usr/include -m64 \
  CMakeFiles/dbusmenuexportertest.dir/dbusmenuexportertest.cpp.o \
  CMakeFiles/dbusmenuexportertest.dir/testutils.cpp.o  \
  -o dbusmenuexportertest -rdynamic -lQtGui -lQtCore -lQtDBus -lQtTest \
  ../src/libdbusmenu-qt.so.2.6.0 -lQtGui -lQtCore -lQtDBus \
  -Wl,-rpath,/nfs/Srcs/L/Libs/Dbusmenu-qt/build/src

となっているようで、実際にこのコマンドをスクリプトにして実行しても同じエラーになりました。一方、問題となっているDBusMenuExporter::DBusMenuExporter(QString...)という関数はどのライブラリが持っているのだろう…、と調べると、このソフトウェア自体が作っているlibdbusmenu-qt.so.2.6.0の中にありました。

$ nm  -C ../src/libdbusmenu-qt.so.2.6.0 | grep DBusMenuExporter::DBusMenuExporter
0000000000009882 t DBusMenuExporter::DBusMenuExporter\
  (QString const&, QMenu*, QDBusConnection const&)
0000000000009882 t DBusMenuExporter::DBusMenuExporter\
  (QString const&, QMenu*, QDBusConnection const&)

さて、そうなると上記リンク時のコマンドでこのライブラリは正しく参照されているなずなのに、なぜ"undefined reference"エラーになるのでしょう? ライブラリを参照する順番を変えてみたり、libdbusmenu-qt.so.2.6.0を明示的にライブラリとして参照させたりしても状況は変わりませんでした。

おかしいな…、と思ってQtを古いバージョン(4.7.1)に戻してみると、この部分も問題なく通ってビルドは終了します。どうやら問題はlibdbusmenu-qtではなくQtの側にありそうだ、ということまでは分かりましたが、Qt-4.8.1の何が問題なのかは分かりません。

Qt-4.8.1のビルドでは、以前当てていたローカルなパッチは外してBLFSのパッチやオプション指定を流用するようにしているので、ソースコードレベルで問題があるようにも思えません。当初は、nmコマンドでライブラリを調べるとシンボルが見つからないと表示されることが原因か、とも考えて

$ nm /usr/lib64/libQtCore.so.4.8.1 
nm: /usr/lib64/libQtCore.so.4.8.1: no symbols

ライブラリから不要なシンボルを取り除くコマンドをstrip --un-neededからstrip --debugに変更し、シンボルを見えるようにビルドし直してみても同じところでリンクに失敗します。

あれこれ思いつく対策は試してみましたが、どうにも解決できないので、Qt-4.8.1への更新は見送ろうか…、とも思い始めました。

QT_VISIBILITY_AVAILABLE is not available

ところが解決は以外なところで見つかりました。

とりあえずビルドできないlibdbusmenu-qtは放置して、本命のKDE-4.8.3の一部であるkdelibsをビルドすればどうなるだろう、と試してみたところ、CMakeがビルド環境をチェックしていく途中で以下のようなエラーが表示されました。

Run Build Command:/usr/bin/make "cmTryCompileExec658130683/fast"
/usr/bin/make -f CMakeFiles/cmTryCompileExec658130683.dir/build.make \
  CMakeFiles/cmTryCompileExec658130683.dir/build
make[1]: ディレクトリ `/nfs/Srcs/K/KDE/4.8.3/Kdelibs/build/CMakeFiles/CMakeTmp' に入ります
/usr/bin/cmake -E cmake_progress_report \
 /nfs/Srcs/K/KDE/4.8.3/Kdelibs/build/CMakeFiles/CMakeTmp/CMakeFiles 1
....
/usr/bin/g++  -isystem /usr/include -m64   -Wnon-virtual-dtor \
 -Wno-long-long -ansi -Wundef -Wcast-align -Wchar-subscripts -Wall \
 -W -Wpointer-arith -Wformat-security -fno-exceptions -DQT_NO_EXCEPTIONS...

/nfs/Srcs/K/KDE/4.8.3/Kdelibs/build/CMakeTmp/check_qt_visibility.cpp:5:3: \
 エラー: #error QT_VISIBILITY_AVAILABLE is not available
make[1]: ディレクトリ `/nfs/Srcs/K/KDE/4.8.3/Kdelibs/build/CMakeFiles/CMakeTmp' から出ます
make[1]: *** [CMakeFiles/cmTryCompileExec658130683.dir/check_qt_visibility.cpp.o] エラー 1
make: *** [cmTryCompileExec658130683/fast] エラー 2

CMake Error at cmake/modules/FindKDE4Internal.cmake:1295 (message):
  Qt compiled without support for -fvisibility=hidden.  This will break
  plugins and linking of some applications.  Please fix your Qt installation
  (try passing --reduce-exports to configure).
 Call Stack (most recent call first):
  CMakeLists.txt:50 (find_package)

これは今まで見た記憶のないタイプのエラーでした。原因は、エラーメッセージにあるように、Qtが-fvisibility=hiddenのオプションを付けてビルドされていないことのようです。

メッセージによると、Qtが-fvisibility=hiddenを付けずにビルドされている場合、プラグインの読み込みやアプリケーションのリンク時に失敗することがある、とのことなので、どうやらlibdbusmenu-qtのリンクに失敗していたのもこれが原因だったようです。

メッセージには、configure時に--reduce-exportsという指定をすればいい、と書いてあるので、libdbusmenu-qtのビルドに問題が無かった古いQt-4.7.1ではそのような指定をしていたのだろうか…、と調べ直してみると、configureに与えるオプション指定ではなく、何気なく外していたconfigure_REDUCE_EXPORTS.patchというパッチで、configureスクリプトのデフォルト値を変更していたのでした。

リスト1 configure_REDUCE_EXPORTS.patch
--- qt-kde-qt/configure 2010-09-07 06:05:01.000000000 +0900
+++ build/configure     2011-02-13 22:29:59.000000000 +0900
@@ -730,7 +730,7 @@
 CFG_PRECOMPILE=auto
 CFG_SEPARATE_DEBUG_INFO=auto
 CFG_SEPARATE_DEBUG_INFO_NOCOPY=no
-CFG_REDUCE_EXPORTS=auto
+CFG_REDUCE_EXPORTS=yes
 CFG_MMX=auto
 CFG_3DNOW=auto
 CFG_SSE=auto

なるほどこれだったのか、と合点して、このパッチをあてるようにQt-4.8.1をビルドし直してみたところ、新しいQt-4.8.1の環境下では、無事libdbusmenu-qtもリンクできるようになり、kdelibs-4.8.3のビルドも可能になりました。

GCCの-fvisibilityオプション

さて、とりあえず正しく動作しそうなQt-4.8.1はビルドできたものの、問題の-fvisibility=hiddenとは一体どういう意味のオプションなのでしょうか? Google等で調べたところ、この指定はGCCが生成するELF形式のバイナリファイルのVisibilityという属性を制御する機能だそうです。

ELF形式のバイナリファイルは、⁠Executable and Linking Format⁠という名称通り、実行形式として使うことも可能だし、他のバイナリファイルとリンクして使うことも可能になっており、そのためELF形式のバイナリファイルでは、そのファイルが持っているシンボル(関数や変数の名前)を外部に公開するかどうかをVisibilityという属性を使って細かく制御できるようになっています。

GCC付属のドキュメント(gcc.info)によると、-fvisibilityは、このVisibility属性のデフォルト値を指定するためのオプションで、-fvisibility=hiddenと指定すると、外部に公開することをソースコード中で明示したシンボル以外は外部から見えなくなるように各シンボルのVisibility属性が設定されます。

-fvisibilityオプションを指定しない場合は-fvisibility=defaultと解釈され、この機能を持たない古いGCCでビルドした場合と同様、全てのシンボルが外部に公開されるようになります。

上記ドキュメントによると、大規模なライブラリ、特にクラス継承等で膨大なシンボルを扱うことになるC++で書かれたライブラリの場合、公開するシンボルを記録したテーブルが巨大になり、シンボルの解決に時間がかかってアプリケーションの起動が遅くなるといった問題が起きがちなので、新しく共有ライブラリを開発する場合は、ビルド時に-fvisibility=hiddenを指定して、ソースコードで明示したシンボル以外は公開しないようにすることを強く推奨していました。

今回ハマったlibdbusmenu-qtの問題も、どうやらこのVisibility属性の設定ミスが原因で、-fvisibility=hiddenを指定せずにビルドしたQtを使ったので、本来は外部に公開すべきではないシンボルまで見えてしまってリンカが混乱し、libdbusmenu-qt自身のライブラリに含まれているシンボルを正しく処理できずに"undefined symbol"というエラーになってしまったようです。

それでは、このオプション指定の有無で、どれくらい公開されているシンボル数が変わるのだろう? と思って、上記パッチをあてた場合とそうでない場合それぞれのバージョンのQt-4.8.1を作って調べてみました。

-fvisibility=hiddenを付けない場合のlibQtCore.so.4.8.1は

$ nm -C -D NG/usr/lib64/libQtCore.so.4.8.1 | wc -l
6575

と、6575のシンボルが表示されていました。

一方、このオプションを付けた場合は、

$ nm -C -D OK/usr/lib64/libQtCore.so.4.8.1 | wc -l
4300

となり、付けない場合に比べてシンボル数は2300ほど減少しました。

具体的にどのようなシンボルが見えなくなっているのだろう? と思って、それぞれのライブラリからシンボル名を抽出して比較してみました。

$ nm -C -D NG/usr/lib64/libQtCore.so.4.8.1 | cut -f3- -d' ' | sort > NG_symbols.dat
$ nm -C -D OK/usr/lib64/libQtCore.so.4.8.1 | cut -f3- -d' ' | sort > OK_symbols.dat
$ diff -u NG_symbols OK_symbols
--- NG_symbols.dat      2012-05-26 08:21:41.865942026 +0900
+++ OK_symbols.dat      2012-05-26 08:21:49.148446539 +0900
@@ -224,86 +224,6 @@
                w __cxa_finalize
                w __gmon_start__
                w __pthread_unwind_next
-BackEase::copy() const
-BackEase::value(double)
-BackEase::~BackEase()
...

両者の違いをdiffで見ると、-fvisibility=hiddenを付けた場合はBackEase::copy()やBackEase::value(double)といった関数が見えなくなっているようです。

両者の違いを見ていて、なるほど、と思ったのは、このオプションを指定したライブラリでは、名前の一部にstaticやPrivateという文字列が入った、本来ローカルのみで利用すべきであろう関数が見えなくなっていることでした。

 QAbstractAnimation::qt_metacast(char const*)
-QAbstractAnimation::qt_static_metacall(QObject*, QMetaObject::Call, int, void**)
 QAbstractAnimation::resume()
...
 QAbstractAnimation::staticMetaObject
-QAbstractAnimation::staticMetaObjectExtraData
 QAbstractAnimation::stop()
...
 QAbstractAnimation::~QAbstractAnimation()
-QAbstractAnimationPrivate::setState(QAbstractAnimation::State)
-QAbstractAnimationPrivate::~QAbstractAnimationPrivate()
-QAbstractAnimationPrivate::~QAbstractAnimationPrivate()
-QAbstractAnimationPrivate::~QAbstractAnimationPrivate()
 QAbstractConcatenable::convertFromAscii(char const*, int, QChar*&)
...

これらの関数がソースコード中でどのように定義されているのかまでは調べていませんが、GCC WikiのVisibilityに関する項目では、Visibility属性を設定するためのクロスプラットフォームで使えるマクロを定義しておき、外部から参照してもいい関数はそのマクロを使って属性をdefaultに、そうでない関数はコンパイル時の-fvisibility=hiddenオプションで属性をhiddenにして外部から見えないようにする、といった方法が推奨されているので、恐らくQt-4.8.1のソースコードでもそのような設定になっているのでしょう。

上記GCC Wikiに説明されているように、この機能は元々、クラス継承等でシンボル数が膨大になりがちなC++を念頭に開発された機能で、GCC-3.6のころにパッチとして提案され、4.0以降ではソースツリーに取り込まれて、正式な機能として利用可能になっていたそうです。

個人的に、C++は規模が大きすぎて手に負えない、と敬遠しがちだったこともあって、このような便利な機能が、ずいぶん前から採用されていることをすっかり見落していたので、今回の経験は「目からウロコ」の例となりました。

ディストリビューションのまとめ役を務めるには、日々の修行が欠かせないようです……。

おすすめ記事

記事・ニュース一覧