続・玩式草子 ―戯れせんとや生まれけん―

第25回「君の名は」余録:libc_nonshared.aの

過去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/にインストールされる静的ライブラリはこれくらいあります。

$ ls usr/lib/lib*a
usr/lib/libBrokenLocale.a  usr/lib/libdl.a      usr/lib/libmvec.a
usr/lib/libanl.a           usr/lib/libg.a       usr/lib/libpthread.a
usr/lib/libc.a             usr/lib/libm-2.32.a  usr/lib/libresolv.a
usr/lib/libc_nonshared.a   usr/lib/libm.a       usr/lib/librt.a
usr/lib/libcrypt.a         usr/lib/libmcheck.a  usr/lib/libutil.a

それに対し/lib/にインストールされる共有ライブラリはこうなります。

$ ls lib/lib*so
lib/libBrokenLocale-2.32.so*  lib/libmemusage.so*         lib/libnss_hesiod-2.32.so*
lib/libSegFault.so*           lib/libmvec-2.32.so*        lib/libpcprofile.so*
lib/libanl-2.32.so*           lib/libnsl-2.32.so*         lib/libpthread-2.32.so*
lib/libc-2.32.so*             lib/libnss_compat-2.32.so*  lib/libresolv-2.32.so*
lib/libcrypt-2.32.so*         lib/libnss_db-2.32.so*      lib/librt-2.32.so*
lib/libdl-2.32.so*            lib/libnss_dns-2.32.so*     lib/libthread_db-1.0.so*
lib/libm-2.32.so*             lib/libnss_files-2.32.so*   lib/libutil-2.32.so*

両者を見比べると、libBrokenLocale.aとlibBrokenLocale-2.32.soやlibc.aとlibc-2.32.soのように、たいていの静的ライブラリには対応する共有ライブラリが用意されているのに対し、libc_nonshared.aには対応する共有ライブラリはありません。

一方、GCCがCのソースコードをビルドする際に参照する、/usr/lib/libc.soというファイルは、このようなld用のスクリプトになっています。

 1  /* GNU ld script
 2     Use the shared library, but some functions are only in
 3     the static library, so try that secondarily.  */
 4  OUTPUT_FORMAT(elf64-x86-64)
 5  GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a  AS_NEEDED ( /lib/ld-linux-x86-64.so.2 ) )

これを見ると、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を対象に考察を進めます。

arnmを使ってlibc_nonshared.aの中身を調べると、このライブラリに含まれているオブジェクトファイルは16個、C言語から直接呼び出されそうなシンボルはatexitやstat、fstat等、14個ほどでした。

$ ar t libc_nonshared.a | cat -n
     1        elf-init.oS
     2        atexit.oS
     3        at_quick_exit.oS
     4        pthread_atfork.oS
     5        stat.oS
     6        fstat.oS
     7        lstat.oS
     8        stat64.oS
     9        fstat64.oS
    10        lstat64.oS
    11        fstatat.oS
    12        fstatat64.oS
    13        mknod.oS
    14        mknodat.oS
    15        warning-nop.oS
    16        stack_chk_fail_local.oS

$ nm libc_nonshared.a | grep 000000 | cat -n
     1        0000000000000060 T __libc_csu_fini
     2        0000000000000000 T __libc_csu_init
     3        0000000000000000 T atexit
     4        0000000000000000 T at_quick_exit
     5        0000000000000000 T __pthread_atfork
     6        0000000000000000 W pthread_atfork
     7        0000000000000000 T __stat
     8        0000000000000000 W stat
     9        0000000000000000 T __fstat
    10        0000000000000000 W fstat
    11        0000000000000000 T __lstat
    12        0000000000000000 W lstat
    13        0000000000000000 T stat64
    14        0000000000000000 T fstat64
    15        0000000000000000 T lstat64
    16        0000000000000000 T fstatat
    17        0000000000000000 T fstatat64
    18        0000000000000000 T __mknod
    19        0000000000000000 W mknod
    20        0000000000000000 T mknodat
    21        0000000000000000 t nop
    22        0000000000000000 T __stack_chk_fail_local

機能的な関連性からまとめられたのだろうか、とあれこれ考えてはみたものの、終了時に呼び出す関数を指定する"atexit()"系、ファイルの状況を読み込む"stat()"系、特殊ファイルを作る"mknod()"系、の間に関連があるとは思えません。

glibcのドキュメントやソースコードを眺めてみても、⁠libc_nonshared.aは常に静的リンクされる」程度の情報しかなく、なぜこれらの機能が常に静的リンクされねばならないのかはわかりません。

はてさて、どういう理由で関係なさそうなこれらの機能が1つのライブラリにまとめられたのだろう…… と、あれこれ考えているうち、⁠多分これでは」ということに思いあたりました。

きっかけはobjdumpを用いたライブラリの逆アセンブルです。objdumpはバイナリファイルを操作するためのbinutilsパッケージに含まれるコマンドで、アセンブラの反対、すなわち機械語をアセンブラコードに変換してくれます。

$ objdump -d libc_nonshared.a | cat -n
     1        書庫 libc_nonshared.a 内:
     2       
     3        elf-init.oS:     ファイル形式 elf64-x86-64
     4       
     5       
     6        セクション .text の逆アセンブル:
     7       
     8        0000000000000000 <__libc_csu_init>:
     9           0:   41 57                   push   %r15
    10           2:   4c 8d 3d 00 00 00 00    lea    0x0(%rip),%r15        # 9 <__libc_csu_init+0x9>
    11           9:   41 56                   push   %r14
    12           b:   49 89 d6                mov    %rdx,%r14
    ...
    42          5d:   0f 1f 00                nopl   (%rax)
    43       
    44        0000000000000060 <__libc_csu_fini>:
    45          60:   c3                      retq   
    46       
    ...
    52        0000000000000000 <atexit>:
    53           0:   48 8b 15 00 00 00 00    mov    0x0(%rip),%rdx        # 7 <atexit+0x7>
    54           7:   31 f6                   xor    %esi,%esi
    55           9:   e9 00 00 00 00          jmpq   e <atexit+0xe>
    56       
    ...
    62        0000000000000000 <at_quick_exit>:
    63           0:   48 8b 35 00 00 00 00    mov    0x0(%rip),%rsi        # 7 <at_quick_exit+0x7>
    64           7:   e9 00 00 00 00          jmpq   c <at_quick_exit+0xc>
    65       
   ...
   102        0000000000000000 <__lstat>:
   103           0:   48 89 f2                mov    %rsi,%rdx
   104           3:   48 89 fe                mov    %rdi,%rsi
   105           6:   bf 01 00 00 00          mov    $0x1,%edi
   106           b:   e9 00 00 00 00          jmpq   10 <__lstat+0x10>
   107       
   ...
   213        0000000000000000 <__stack_chk_fail_local>:
   214           0:   48 83 ec 08             sub    $0x8,%rsp
   215           4:   e8 00 00 00 00          callq  9 <__stack_chk_fail_local+0x9>

この結果を見ると、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と肥大します。

$ ls -lh static_nss/usr/lib/libc_nonshared.a
-rw-r--r-- 1 kojima users 266K  8月 16日  08:36 static_nss/usr/lib/libc_nonshared.a

含まれているオブジェクトファイルを見てもNSS関連のファイルが多数追加されていました。

$ ar t  static_nss/usr/lib/libc_nonshared.a | cat -n
     1        elf-init.oS
     2        atexit.oS
     3        at_quick_exit.oS
     ...
    15        warning-nop.oS
    16        stack_chk_fail_local.oS
    17        dns-host.oS
    18        dns-network.oS
    19        dns-canon.oS
    20        res_comp.oS
    ...
    52        files-initgroups.oS
    53        files-init.oS

もちろん、含まれる機能(シンボル)も大幅に増加しています。

$ nm static_nss/usr/lib/libc_nonshared.a | cat -n
     1       
     2        elf-init.oS:
     3                         U _GLOBAL_OFFSET_TABLE_
     4                         U __init_array_end
     5                         U __init_array_start
     6        0000000000000060 T __libc_csu_fini
     7        0000000000000000 T __libc_csu_init
     ...
    86        stack_chk_fail_local.oS:
    87                         U _GLOBAL_OFFSET_TABLE_
    88                         U __stack_chk_fail
    89        0000000000000000 T __stack_chk_fail_local
    90       
    91        dns-host.oS:
    92        0000000000000000 r .LC1
    93                         U _GLOBAL_OFFSET_TABLE_
    94                         U __dn_expand
    ...
   108        0000000000001e90 T _nss_dns_gethostbyaddr2_r
   109        0000000000002650 T _nss_dns_gethostbyaddr_r
   110        00000000000019c0 T _nss_dns_gethostbyname2_r
   111        0000000000001920 T _nss_dns_gethostbyname3_r
   112        0000000000001af0 T _nss_dns_gethostbyname4_r
   113        0000000000001a40 T _nss_dns_gethostbyname_r
    ...
   944        files-init.oS:
    ...
   955        0000000000000000 b netgr_traced_file
   956        0000000000005140 b pwd_traced_file
   957        0000000000002080 b resolv_traced_file
   958        0000000000001040 b serv_traced_file

この結果を見ると、"--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には使用頻度は高いもののサイズが小さい機能が集められている」という仮説は、状況証拠から考えた筆者の推論で明確な根拠はありません。このあたりを扱ったドキュメントや議論についてご存知の方がおられましたら、ぜひ筆者までお知らせください。

おすすめ記事

記事・ニュース一覧