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

第24回 君の名は(NSS:Name Service Switch)[その2]

この記事を読むのに必要な時間:およそ 6 分

インターミッション

UNIXの世界では,各ユーザはユーザID(uid)という数字で管理されています。しかしながら,人間にとっては数字だけでは扱いにくいので,uidに結びつけたユーザ名を利用できるようになっています。このマッピングに使われるのが/etc/passwdファイルです。

uidとユーザ名以外にも,gidとグループ名,IPアドレスとホスト名など,数字と名称を結び付けるための設定ファイルは他にもいくつかあるものの,ここではそれらの代表例として/etc/passwdを使っています。

元々,/etc/passwdはそれぞれのコンピュータごとに用意していたものの,ユーザやコンピュータが増えて,さらにそれらがネットワークに接続されていくと,データの更新やコンピュータ間でのファイルの同期が大変になります。

そこで考案されたのが"YP(Yellow Pages)"と呼ばれる機能で,/etc/passwdファイルを1台のサーバ機で集中管理し,他のコンピュータは必要に応じてそのサーバに問い合わせよう,という仕組みです。

"Yellow Pages"は英米の「職業別電話帳」いいで,電話帳を使って電話番号を調べることになぞらえて名付けられたものの,"Yellow Pages"という語は商標登録されていたため,後に"NIS(Network Information Service)"と改称されました。

名称はNISに変更されたものの,コマンド名やシステムコール名には"ypbind"や"ypmatch"のように"Yellow Pages(yp)"を冠する名称が残っています。

YP/NISはSun Microsystems社が開発し,業界標準としてUNIX系OSで広く採用されています。一方,Linuxが採用しているglibc2では,後発なこともあって単にNISの機能を再現するだけでは飽き足らず,⁠名前解決」の対象範囲を広げるとともに,解決方法や適用順も自由に変更できるような実装にして,NSS(Name Service Switch)と呼ぶようになりました。

NSSの特徴のひとつが,名前解決の方法を共有ライブラリとして簡単に追加できることです。

たとえば,Plamo Linuxではネットワークプリンタ等を見つけるために,Avahiと呼ばれるマルチキャストDNS(mDNS)サーバを採用しています。

mDNSサービスはネットワークに接続した機器に自動的にホスト名を割り当てる仕組みで,対応している機器同士は/etc/hostsやDNSサーバを介さずに通信できるようになります。

そして,このmDNSサービスを利用するためのNSSプラグインがnss_mdnsというパッケージです。

このプラグインの本体はlibnss_mdns[4,6,_minimal].so.2という共有ライブラリで,このライブラリを適切なディレクトリに配置し,/etc/nsswitch.conf のhosts行にmDNSを参照するような指示を追加するだけでmDNSサービスが利用可能になります。

Plamo Linuxではnsswitch.confのhosts行はこのようにしています。

hosts:            files mdns4_minimal [NOTFOUND=return] dns mdns4

この設定で,ホスト名とIPアドレスの参照が必要になった場合,/etc/passwdを調べるfilesmDNSサーバAvahiに問い合わせるmdns4_minimalDNSサーバに問い合わせるdnsそれでも見つからなければmdns4という順でNSS用モジュールが適用されることになります。

なお,"mdns4_minimal [NOTFOUND=return]"という指定は,調べたいホスト名がmDNSの命名ルールに基づく"XXXX.local"だった場合にのみ適用し,[NOTFOUND=return]は,その名前がmDNSサーバ(Avahi)で解決できなければそのまま終了せよ,という指示です。この設定により,mDNS用の名前である"XXXX.local"をDNSサーバに問い合わせることを防いでいます。

glibc2では,このように名前解決の手段をプラグインで追加できることがウリのひとつとなっているため,"--enable-static"で明示的に指定しないと,static linkしたバイナリも"libnss_files.so"等の共有ライブラリを見に行くという仕様になっているようです。

解決

まずconfigureオプションに"--enable-static-nss"を追加してglibc-2.32をビルドし直し,指定せずにビルドした場合と比較したところ,作成されるライブラリの数や種類は同じものの,static link時に利用されるlibc.alibc_nonshared.aのサイズがだいぶ増加しました。

具体的なサイズを見てみると,"--enable-static-nss"を指定しなかった場合,libc.aが5.2MB,libc_nonshared.aは23KBでした。

$ ls -lh default_nss/usr/lib/libc*.a
-rw-r--r-- 1 kojima users 5.2M  8月  7日  08:06 default_nss/usr/lib/libc.a
-rw-r--r-- 1 kojima users  23K  8月  7日  08:06 default_nss/usr/lib/libc_nonshared.a

一方,"--enable-static-nss"を指定してビルドすると,libc.aが5.4MB,libc_nonshared.aが266KBで,それぞれ200KBほど増加しました。

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

実際にどのような機能が増えたのかをlibc.aに含まれるシンボル名から調べてみたところ,今回問題となった"nss_files"に関連するシンボルは,前者では8だけだったのに対し,後者では143まで増加していました。

$ nm default_nss/usr/lib/libc.a |& grep nss_files | wc -l
  8
$ nm static_nss/usr/lib/libc.a |& grep nss_files | wc -l
  143

具体的に,どのような機能(=シンボル)が追加されたのかを確認してみると,"nss_files_fopen.o"というファイルの中に,実際にファイルを読む等の処理を行うgethostbyaddrgetpwentといったシンボルが多数含まれていました。

$ nm static_nss/usr/lib/libc.a |& grep nss_files | cat -n
  ...
  59  nss_files_fopen.o:
  60  0000000000000000 T __nss_files_fopen
  61                   U __nss_files_fopen
  62  0000000000000310 T _nss_files_endprotoent
  63  0000000000000520 T _nss_files_getprotobyname_r
  64  00000000000006d0 T _nss_files_getprotobynumber_r
  65  0000000000000370 T _nss_files_getprotoent_r
  66  0000000000000000 T _nss_files_parse_protoent
  67  0000000000000280 T _nss_files_setprotoent
  68                   U __nss_files_fopen
  69  0000000000000360 T _nss_files_endservent
  70  0000000000000570 T _nss_files_getservbyname_r
  ....
 141  0000000000000000 T _nss_files_initgroups_dyn
 142                   U _nss_files_parse_grent
 143  0000000000000000 T _nss_files_init

一方,"--enable-static-nss"オプションを指定せずにビルドしたlibc.aでは,実際の処理を行うためのコードはほとんど見あたりません。

$ nm default_nss/usr/lib/libc.a |& grep nss_files | cat -n
   1  0000000000000000 T _nss_files_parse_grent
   2  0000000000000000 T _nss_files_parse_pwent
   3  0000000000000000 T _nss_files_parse_spent
   4                   U _nss_files_parse_spent
   5  0000000000000000 T _nss_files_parse_sgent
   6                   U _nss_files_parse_sgent
   7  nss_files_fopen.o:
   8  0000000000000000 T __nss_files_fopen

どうやらこのlibc.aを使えば共有ライブラリを呼び出さずにNSS機能が使えそうなので,さっそくcoreutils-8.32のlsをstatic linkでビルドし,組み込まれるシンボルを確認しました。

$ nm static-build/src/ls | grep nss | cat -n
   1  000000000053a2a0 V __nss_aliases_database
   2  0000000000472d40 T __nss_configure_lookup
  ...
  68  0000000000473890 T _nss_files_getprotobyname_r
  69  0000000000473a40 T _nss_files_getprotobynumber_r
  70  00000000004736e0 T _nss_files_getprotoent_r
  71  0000000000476bf0 T _nss_files_getpwent_r
  72  0000000000476da0 T _nss_files_getpwnam_r
  73  0000000000476f30 T _nss_files_getpwuid_r
  ...
 101  0000000000477fb0 T _nss_netgroup_parseline
 102  0000000000472490 t nss_parse_service_list

static linkしたlsに組み込まれているシンボルは102あり,_nss_files_getpwent_r等,/etc/passwdを読むための機能も含まれているようです。

念のため,"--enable-static-nss"オプションを使わずに作成したglibc-2.30な環境(Plamo-7.2のデフォルト)で同じcoreutils-8.32をビルドして確認したところ,含まれるNSS関連のシンボルは26ほどで,"getpwent"等,/etc/passwdを実際に読み込む機能は共有ライブラリ(libnss_files.so)に任せているようです。

  $ nm src/ls | grep nss | cat -n
     1  000000000051d8e8 V __nss_aliases_database
     2  0000000000479be0 T __nss_configure_lookup
    ...
    19  000000000051d8a8 V __nss_passwd_database
    20  000000000047a2c0 T __nss_passwd_lookup2
    ...
    24  000000000051d888 V __nss_services_database
    25  000000000051d880 V __nss_shadow_database
    26  0000000000479330 t nss_parse_service_list

この"--enable-static-nss"なglibc2環境で作ったstatic linkのlsは,NSS回りの機能が自前で完結しているため,glibc-2.32以前の環境でも"Assertion failure"になったり,/lib/libnss_files.soを呼び出したりすることなく動作しました。

$ ls  /lib/libc*
/lib/libc-2.30.so*  /lib/libcap.so.2@     /lib/libcrack.so.2@      /lib/libcrypt-2.30.so*
/lib/libc.so.6@     /lib/libcap.so.2.27*  /lib/libcrack.so.2.9.0*  /lib/libcrypt.so.1@
$ strace ./ls -l |& grep openat | cat -n
   ...
   5  openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 4
   6  openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 4
   7  openat(AT_FDCWD, "/etc/group", O_RDONLY|O_CLOEXEC) = 4
   8  openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
   ...

このように,glibc-2.32を"--enable-static-nss"オプションで作り直し,その環境でインストーラ用のstatic linkなバイナリファイルを作り直すことで,名前解決機能がglibc2のバージョンに依存する問題が解決できました。


2回に渡って,glibc-2.32の更新時に発生したトラブルを例に,nmやstraceを使ったバイナリデバッグの例を紹介してみた結果,リストや操作例が多くなってしまい,記事として読みづらくなったことをお詫びします。

Linuxに代表されるOSSの世界ではたいていのソースコードは公開されているので,printfデバッグやステップトレースで追いかけることも可能なものの,glibc2のような大規模なソフトウェアの場合,ソースコードをコツコツと調べるよりも,作成されたオブジェクトファイルを直接解析する方が手っ取り早いこともあります。

筆者自身,この分野にはまだまだ未熟なものの,機会があればこのような"binary hack"も紹介してみたいと思っています。

著者プロフィール

こじまみつひろ

Plamo Linuxとりまとめ役。もともとは人類学的にハッカー文化を研究しようとしていたものの,いつの間にかミイラ取りがミイラになってOSSの世界にどっぷりと漬かってしまいました。最近は田舎に隠棲して半農半自営な生活をしながらソフトウェアと戯れています。

URLhttp://www.linet.gr.jp/~kojima/Plamo/index.html