本連載は「ソースコード・リテラシー」と称しつつ、サイズや規模の関係からCのソースコードそのものを取りあげる機会があまりありませんでした。最近、Plamo-4.51の準備をしている最中に、わりと面白い(?)事例に遭遇したので、ソースコード・リテラシーの実践編として紹介してみようと思います。
今回紹介するのはpmount というコマンドをパッケージ化する際に遭遇したトラブルです。実のところトラブルの原因はこのソフトウェアとは違うところにあったのですが、そこに辿りつくまでの過程はトラブル解決の実例として面白いと思い、ここに紹介する次第です。
pmountとは?
伝統的なUNIXの考え方では、ファイルシステムの構成を変更するmount コマンドはrootユーザの権限で実行すべきコマンドで、一般ユーザからは利用できないようになっています。
UNIXのこの考え方は、ファイルシステムを構成するデバイスがHDDしか無かった時代の産物で、CD-ROMやフロッピーディスクのような、一般ユーザの権限でも読み書きしたいメディアをうまく扱うことができません。そのため、伝統的なUNIX/Linuxの世界では、/etc/fstab のマウントオプションフィールドにuserを指定したデバイスは、一般ユーザの権限でもマウントできるように機能を拡張しています。
たとえば、筆者がメンテナンスしているPlamo Linuxでは、/dev/cdromは一般ユーザでもマウントできるように、/etc/fstabに以下のような設定を追加しています。4つめの欄がマウントオプションフィールドで、このデバイスをマウントする際には、ここで指定したオプションが適用されます。
/dev/cdrom /cdrom iso9660 user,ro,noauto,exec,iocharset=euc-jp 0 0
CD-ROMやフロッピーディスクのように、使用するデバイスファイル(上記の例では/dev/cdrom)が決まっているデバイスならば /etc/fstab の設定で対応可能ですが、USBメモリやコンパクトフラッシュのように、活線挿抜が可能で、接続する度に対応するデバイスファイルが変化するデバイスでは、あらかじめデバイスファイルを登録しておくことができません。
Linuxカーネルでは、USBメモリ等の記憶デバイスはSCSI HDDと認識され、すでに接続されているデバイスファイルと競合しないように/dev/sdb*や/dev/sdc*が自動的に割りあてられます。割り当てられるデバイスファイルは使用状況によって変化するため、事前に/etc/fstabに設定することは困難です。
pmount (policy mount)コマンドはこのような問題を解決するためにDebian方面で開発されたコマンドで、USBメモリ等の活線挿抜可能なデバイスに限定して、一般ユーザでもマウントコマンドを実行できるようにするものです。pmountはmountコマンドのラッパーとして働き、マウントしようとしているデバイスやマウントポイントをチェックした上で、mountコマンドを実行するような作りになっています。
トラブル発生
pmountは現在も開発が続いており、最新のソースコードはgit経由で入手できました。入手したソースコードを見ると、configureスクリプトこそありませんでしたが、configureスクリプトを生成するためのautogen.shが用意されていたので、autogen.shを実行してconfigureを生成し、とくに問題なくコンパイルできました。
ところが手元にあったUSBメディアプレイヤーを挿してテストしてみると、正しく動作してくれません。
% ./work/usr/bin/pmount /dev/sda1
Error: device /dev/sda1 is not removable
直接mountコマンドを使えば問題なくマウントできます。
# mount /dev/sdb1 /media
# df
Filesystem 1K-ブロック 使用 使用可 使用% マウント位置
/dev/hda3 19236340 16601960 1657228 91% /
none 1037060 168 1036892 1% /dev
....
/dev/sdb1 942864 885448 57416 94% /media
しばらくドキュメントを調べたり、システムのログファイルを眺めたりしましたが、カーネルレベルでのデバイスの認識などには異常なく、どうもpmount自身の問題のようです。
pmountのヘルプメッセージを見ると、-dというデバッグ出力用のオプションが用意されているので、試してみました。
% ./work/usr/bin/pmount -d /dev/sdb1
resolved /dev/sdb1 to device /dev/sdb1
Checking for device '/dev/sdb1' in '/etc/fstab'
-> not foundmount point to be used: /media/sdb1
no iocharset given, current locale encoding is EUC-JP
Cleaning lock directory /var/lock/pmount_dev_sdb1
Checking for device '/dev/sdb1' in '/etc/mtab'
-> not foundChecking for device '/dev/sdb1' in '/proc/mounts'
-> not founddevice_whitelist: checking /etc/pmount.allow...
device_whitlisted(): nothing matched, returning 0
find_sysfs_device: looking for sysfs directory for device 8:17
find_sysfs_device: checking whether /dev/sdb1 is on /sys/block/ram0 (1:0)
find_sysfs_device: checking whether /dev/sdb1 is on /sys/block/ram1 (1:1)
...
find_sysfs_device: checking whether /dev/sdb1 is on /sys/block/sda (8:0)
find_sysfs_device: major device numbers match
find_sysfs_device: minor device numbers do not match, checking partitions...
find_sysfs_device: checking whether device /dev/sdb1 matches partition 8:0
...
find_sysfs_device: checking whether device /dev/sdb1 matches partition 8:17
find_sysfs_device: -> partition matches, belongs to block device /sys/block/sdb
device_removable: could not find a sysfs device for /dev/sdb1
Error: device /dev/sdb1 is not removable
policy check failed
-dオプションを指定するとずいぶん詳細なデバッグメッセージを出してくれました。このメッセージを見る限り、新しく装着したUSBメディアプレイヤーをsysfs経由で認識するところまではうまく行っているようですが、最後のリムーバブルメディアか否かの判断でエラーになっているようです。
pmount がどうやってリムーバブルメディアか否かを判断しているかは、ソースコードを見てみる方がよさそうです。
問題の特定と追跡
まずはエラーメッセージを手がかりに、どの部分がエラーを出しているかをgrep コマンドで調べます。ソースコード全体をチェックしたいので、find やxargs と組み合わせて実行しました。
% find src | xargs grep ' is not removable'
src/policy.c: fprintf( stderr, _("Error: device %s is not removable\n"), device );
デバイスファイル名(/dev/sdb1)の部分は変数になっているだろうから、エラーメッセージの後半部、'is not removable 'を手がかりにsrcディレクトリ以下のファイルを全て調べたところ、src/policy.c にこのメッセージを出力している部分が見つかりました。該当箇所はこのファイルの463行目、device_removable 関数内でした。
リスト1 src/policy.cのdevice_removable関数
457 int
458 device_removable( const char* device )
459 {
460 int removable = device_removable_silent(device);
461
462 if( !removable )
463 fprintf( stderr, _("Error: device %s is not removable\n"), device );
464
465 return removable;
466 }
467
この関数はdevice_removable_silent()という処理を実行した結果を変数removableに入れて、それが0ならエラーメッセージを出すだけのようなので、問題の箇所はむしろdevice_removable_silent() の方のようです。この関数は、device_removable()の直前にありました。
リスト2 src/policy.cのdevice_removable_silent関数
430 /* The silent version of the device_removable function. */
431 int device_removable_silent(const char * device)
432 {
433 struct sysfs_device *dev;
434 static char* hotplug_buses[] = { "usb", "ieee1394", "mmc", "pcmcia", NULL };
435 int removable;
436 char blockdevpath[PATH_MAX];
437
438 dev = find_sysfs_device( device, blockdevpath, sizeof( blockdevpath ) );
439 if( !dev ) {
440 debug( "device_removable: could not find a sysfs device for %s\n", device );
441 return 0;
442 }
443
444 debug( "device_removable: corresponding block device for %s is %s\n",
445 device, blockdevpath );
446
447 /* check whether device has "removable" attribute with value '1' */
448 removable = get_blockdev_attr( blockdevpath, "removable" );
449
450 /* if not, fall back to bus scanning (regard USB and FireWire as removable) */
451 if( !removable )
452 removable = find_bus_ancestry( dev, hotplug_buses );
453 sysfs_close_device( dev );
454 return removable;
455 }
この関数はデバイス名を引数に取って整数値 removable を返す作りになっています。removableに値を入れているのは 448 行目で、そこで値が入らなければ 452 行目で再チェックしているようです。まずは 448行目で呼び出しているget_blockdev_attr() 関数を調べてみます。
リスト3 src/policy.cのget_blockdev_attr関数
247 /**
248 * Return whether attribute attr in blockdevpath exists and has value '1'.
249 */
250 int
251 get_blockdev_attr( const char* blockdevpath, const char* attr )
252 {
253 char path[PATH_MAX];
254 FILE* f;
255 int result;
256 char value;
257
258 snprintf( path, sizeof( path ), "%s/%s", blockdevpath, attr );
259
260 f = fopen( path, "r" );
261 if( !f ) {
262 debug( "get_blockdev_attr: could not open %s\n", path );
263 return 0;
264 }
265
266 result = fread( &value, 1, 1, f );
267 fclose( f );
268
269 if( result != 1 ) {
270 debug( "get_blockdev_attr: could not read %s\n", path );
271 return 0;
272 }
273
274 debug( "get_blockdev_attr: value of %s == %c\n", path, value );
275
276 return value == '1';
277 }
get_blockdev_attr() は同じファイルの 251 行目にありました。この関数はblockdevpathとattrを引数に取って、それらをパス名とするファイルを開き、その値を調べて value を返すようです。
276行目まで進めば1が返るのでdevice_removable_silent() のremovableが1となり、device_removable()の462行目のチェックも通って、リムーバブルメディアであると認識される、という流れのようです。
これらの関数を眺めた限りではdevice_removable() → device_removable_silent() → get_blockdev_attr() と進んでいき、リムーバブルメディアの場合は1が返ってくるのが正しい流れで、この流れのどこかでエラーが生じて1が返らないために、手元のUSBメディアプレイヤーがremovableではないと判断されてしまっているようです。
さて、どこでトラブっているのだろうなぁ……、としばし悩みましたが、get_blockdev_attr() が実行されれば、3ヵ所のdebug文(262、270、274行目)のどこかで "get_blockdev_attr: " を行頭に持つデバッグメッセージが出力されるはずです。しかし、先に見たpmount -d /dev/sdb1 のログではそのようなメッセージは出ていません。ということは、処理はget_blockdev_attr() まで来ていないので、device_removable_silent() が怪しそうです。
改めてこの関数を眺めると、先のデバッグメッセージには440行目のdevice_removable: could not find a sysfs device ... というデバッグ文が出力されているので、439行目のif ( !dev )のチェックで引っかかっていることがわかりました。このdevは438行目でfind_sysfs_device() の結果として帰ってくる値です。
それでは、とfind_sysfs_device() を調べてみました。この関数はリストを載せるには大きすぎるので詳細は示しませんが、デバッグメッセージに出力されていた"find_sysfs_device: "を手がかりに処理を追ってみたところ、/dev/sdb1に該当するsysfsのデータを調べるところまではうまく行っているものの、最後のリンクを辿るところがうまく行かずに、sysfsの情報を示す構造体のデータが取れていないようです。
リスト4 src/policy.cのfind_sysfs_device関数
229 snprintf( devfilename, sizeof( devfilename ), "%s/device", devdirname );
230
231 /* read out the link */
232 if( !sysfs_get_link( devfilename, linkfilename, 1024 ) )
233 sysdev = sysfs_open_device_path( linkfilename );
234
このsysfs_get_link() やsysfs_open_device_path() という関数はpmount の中にはなく、sysfsutils パッケージが提供するlibsysfs の中にあるようです。
% nm /usr/lib/libsysfs.so.2.0.0 | grep sysfs_get_link
00001f70 T sysfs_get_link
% nm /usr/lib/libsysfs.so.2.0.0 | grep sysfs_open_device_path
00005450 T sysfs_open_device_path
必要なライブラリが正しくリンクされていないのかな、とlddコマンドでリンクされるライブラリを調べても、libsysfs.so.2はきちんとリンクされています。
% ldd work/usr/bin/pmount
linux-gate.so.1 => (0xffffe000)
libsysfs.so.2 => /usr/lib/libsysfs.so.2 (0xb7f58000)
libblkid.so.1 => /lib/libblkid.so.1 (0xb7f4f000)
libc.so.6 => /lib/libc.so.6 (0xb7e21000)
libuuid.so.1 => /lib/libuuid.so.1 (0xb7e1d000)
/lib/ld-linux.so.2 (0xb7f89000)
ライブラリの呼び出し方でも変更されたのかなぁ、とsysfsutilsを調べてみると、2.1.0というバージョンがリリースされていましたが、libsysfs を新しいバージョンに入れ替えてもpmountの動作には変化なしでした。
ソースコードを調べるのも手詰りになったので、さてどうしたものか、とpmount とlibsysfs をキーワードに Google で検索してみると、gentooのbugzillaで同じ問題が議論されていること を見つけました。
このページによると、libsysfs はバージョンごとに変るカーネルの sysfs 機能に対応できていないので、使うべきではないこと(カーネルソースに含まれるDocumentation/sysfs-rules.txt にも "Do not use libsysfs" という記述がありました)、カーネルのビルド時にCONFIG_SYSFS_DEPRECATED を定義すればlibsysfsが想定している旧来の形式と互換性を保つようになること、Debian方面でlibsysfsへのパッチが公開されていること などがわかりました。
こうなると、選択肢としては、「 libsysfsとpmountは使わない」 、「 カーネルのビルド時にCONFIG_SYSFS_DEPRECATEDオプションを指定した上でオリジナルのlibsysfsとpmountを使う」 、「 libsysfsにパッチをあててpmountを使う」という3つがありそうです。
いずれの方法もメリット、デメリットがありますが、今回は「libsysfsにパッチをあててpmountを使う」方法を取ることにし、パッチをあてた libsysfs を作成、その元では pmount が正しく動作することを確認しました。
% ./work/usr/bin/pmount /dev/sdb1
Error: device /dev/sdb1 is not removable
% su
パスワード: XXXXX
# updatepkg sysfsutils-2.1.0_1-i586-P1.tgz
removing sysfsutils-2.l.0
Removing package sysfsutils...
Removing files:
--> Deleting symlink usr/lib/libsysfs.so
...
sysfsutils-2.1.0_1-i586-P1 のインストール中
PACKAGE DESCRIPTION:
sysfsutils-2.1.0_1-i586-P1 のインストールスクリプトを実行中
# exit
% ./work/usr/bin/pmount /dev/sdb1
% df
Filesystem 1K-ブロック 使用 使用可 使用% マウント位置
/dev/hda3 19236340 16605608 1653580 91% /
none 1037060 168 1036892 1% /dev
/media 1037060 0 1037060 0% /media
...
/dev/sdb1 942864 885448 57416 94% /media/sdb1
このデバイスはpumountで正しくアンマウントされ、pmountが生成したマウントポイント(/media/sdb1)も消去されました。
冗長になるので問題の背景は省きましたが、Plamo-4.51で採用しようとしている新しい(2.6.27系)カーネルでは、リムーバブルメディアではない通常のHDDもudevがリムーバブルメディアのように処理しようとしてしまう問題が報告され、その対応のためにリムーバブルメディアの操作にはpmountを使うつもりでpmountをパッケージ化しました。その際、udevの新しいバージョンも調べましたが、udevのドキュメントには「CONFIG_SYSFS_DEPRECATED は指定すべきではない」とありました。それらを勘案して、libsysfsにパッチをあてる方法を採用したのですが、あとから見直してみると、pmountを使わなくてもudevルールを改良すればリムーバブルメディアを見分けることができそうなので、libsysfsとpmountを捨てるのも手だったかな……と思っているところです。
今回は比較的規模の小さいソフトウェアを例に、Cのソースコードを辿りながら、問題点を調べていく例を紹介しました。今回取りあげたpmountは、ソースコードのコメントも豊富で、デバッグメッセージも詳細だったので、比較的スムーズに原因の特定や対応方法を見い出すことができました。このあたりが不十分なソフトウェアでは、ソースコードの重要そうなポイントにprintf文を追加して進行状況を確認したり、デバッガでステップトレースしながら変数の値を確認するような作業が必要になるでしょう。
Cで書かれたソースコードの場合、構成しているファイルが膨大な数に昇り、それら全てを調べることは不可能なことも多いでしょう。そのような大規模なソフトウェアでは、出力メッセージを手がかりに問題となっているファイルや行を調べ(出力メッセージが不十分ならばソースコードレベルでデバッグメッセージを追加して) 、そこから処理の流れを遡っていくのが、一見遠回りながら、最も効率的な方法だと思っています。