過去2回に渡ってPlamo Linuxのインストーラで生じたglibcのNSS回りのトラブルを調べてきました。その結果、glibcではNSS回りの機能は静的リンクの対象とならず、静的リンクした実行可能ファイルでも共有ライブラリを参照する、という仕様になっていることがわかりました。
このトラブル自体は"--enable-static-nss"というオプションを与えてglibc-2.32をビルドし直すことで対応できたものの、このオプションを指定するとlibc.aのみならず、libc_nonshared.aというライブラリもサイズが増加します。
libc.aは静的リンク時に参照されるライブラリなので、NSS回りの機能を収めるための指定でサイズが増えるのは理解できるものの、libc_nonshared.aというライブラリまで増量する理由はよくわかりません。そもそもlibc_nonshared.aというライブラリについては何も知らなかったので、この機会に調べてみることにしました。
libc_nonshared.aとは
多くの人にはその存在自体知られていないように思うものの、libc_nonshared.aはglibcの一部として作成されるライブラリで、Plamo Linuxではlibcパッケージ、RPMやDEB系ではglibc-develパッケージに含まれ、CやC++のコードをコンパイルする際に利用される静的(static)ライブラリです。筆者は、Plamo Linuxのglibcパッケージを作る際、以前からこの奇妙な名前(libc_nonshared)を持つライブラリが気になっていました。
というのも、最近のLinux環境では、提供されるライブラリはほぼ全て共有(shared)ライブラリで、静的ライブラリを提供する例はまず無いからです。
加えて、たいていの静的ライブラリには対応する共有ライブラリが存在するのに、このライブラリはその名前(nonshared)通り、対応する共有ライブラリは存在しません。
たとえばglibc-2.32の場合、/usr/lib/にインストールされる静的ライブラリはこれくらいあります。
それに対し/lib/にインストールされる共有ライブラリはこうなります。
両者を見比べると、libBrokenLocale.aとlibBrokenLocale-2.32.soやlibc.aとlibc-2.32.soのように、たいていの静的ライブラリには対応する共有ライブラリが用意されているのに対し、libc_nonshared.aには対応する共有ライブラリはありません。
一方、GCCがCのソースコードをビルドする際に参照する、/usr/lib/libc.soというファイルは、このようなld用のスクリプトになっています。
これを見ると、libc_nonshared.aは共有ライブラリ(libc.so.6)と共に常に参照される重要なライブラリのように思えます。
このようにlibc_nonshared.aはglibc環境ではかなり特殊な位置を占めていそうなものの、23KBほどと小さいこともあり、従来はあまり注意を払っていませんでした。一方、"--enable-static-nss"を指定してglibc-2.32をビルドし直すと、一気に200KB以上サイズが増加してしまいます。果してこれは何故なのでしょう?
libc_noshared.aの中身
後述するように"--enable-static-nss"を指定すると機能が増えて複雑になるので、まずはデフォルトの状態のlibc_nonshared.aを対象に考察を進めます。
arやnmを使ってlibc_nonshared.aの中身を調べると、このライブラリに含まれているオブジェクトファイルは16個、C言語から直接呼び出されそうなシンボルはatexitやstat、fstat等、14個ほどでした。
機能的な関連性からまとめられたのだろうか、とあれこれ考えてはみたものの、終了時に呼び出す関数を指定する"atexit()"系、ファイルの状況を読み込む"stat()"系、特殊ファイルを作る"mknod()"系、の間に関連があるとは思えません。
glibcのドキュメントやソースコードを眺めてみても、「libc_nonshared.aは常に静的リンクされる」程度の情報しかなく、なぜこれらの機能が常に静的リンクされねばならないのかはわかりません。
はてさて、どういう理由で関係なさそうなこれらの機能が1つのライブラリにまとめられたのだろう…… と、あれこれ考えているうち、「多分これでは」ということに思いあたりました。
きっかけはobjdumpを用いたライブラリの逆アセンブルです。objdumpはバイナリファイルを操作するためのbinutilsパッケージに含まれるコマンドで、アセンブラの反対、すなわち機械語をアセンブラコードに変換してくれます。
この結果を見ると、libc_nonshared.aに含まれる機能のうち、"__libc_csu_init"はそれなりのサイズ(といってもおよそ100バイト)なものの、残りの機能はアセンブラで数行、機械語コードでは十数バイト程度とごく小さい規模です。これくらいのサイズなら、共有ライブラリの一部として実行時に一々読み込むよりも、リンク時にあらかじめ実行可能ファイルに組み込んでしまった方がファイルアクセスやロード時間、メモリ展開時間等を含めた全体的なランニングコストは安くなる、glibcの開発者たちはそのように判断して、これらサイズが小さく、使用頻度が比較的多い機能は常に静的リンクされるよう、libc_nonshared.aにまとめたのではないでしょうか。
"--enable-static-nss"とlibc_nonshared.a
一方、"--enable-static-nss"を指定してビルドしたglibcでは、libc_nonshared.aが266KBと肥大します。
含まれているオブジェクトファイルを見てもNSS関連のファイルが多数追加されていました。
もちろん、含まれる機能(シンボル)も大幅に増加しています。
この結果を見ると、"--enable-static-nss"オプションを指定したglibcでは、NSS回りの機能の多くが"libc_nonshared.a"に移され、この環境でソースコードをコンパイルすると、"-static"オプションの有無にかかわらず、実行可能ファイルにNSS関連機能の多くが静的リンクされてしまうようです。
前回紹介したNSSの特長である「追加された共有ライブラリを動的に読み込む機能(nss_load_library)」などは、/lib/libc-2.32.soの方に収められているので実害は無さそうなものの、開発者たちが"not recommended"と言うように、常に静的リンクされるlibc_nonshared.aに多数の機能を詰め込んだ状態はあまり好ましくないでしょう。
NSS回りの機能を静的リンクしてglibcのバージョンに依存しないインストーラ用ツールは作成できたので、次回のglibcのバージョンアップ時には"--enable-static-nss"を指定せずにビルドすることにしました。
今回辿りついた「libc_nonshared.aには使用頻度は高いもののサイズが小さい機能が集められている」という仮説は、状況証拠から考えた筆者の推論で明確な根拠はありません。このあたりを扱ったドキュメントや議論についてご存知の方がおられましたら、ぜひ筆者までお知らせください。