ソースコード・リテラシーのススメ

第22回ソースコード・リテラシー実践編】

本連載は「ソースコード・リテラシー」と称しつつ、サイズや規模の関係から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メモリやコンパクトフラッシュのように、活線挿抜が可能で、接続する度に対応するデバイスファイルが変化するデバイスでは、あらかじめデバイスファイルを登録しておくことができません。

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コマンドで調べます。ソースコード全体をチェックしたいので、findxargsと組み合わせて実行しました。

% 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の動作には変化なしでした。

ソースコードを調べるのも手詰りになったので、さてどうしたものか、pmountlibsysfsをキーワードに 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)も消去されました。

今回は比較的規模の小さいソフトウェアを例に、Cのソースコードを辿りながら、問題点を調べていく例を紹介しました。今回取りあげたpmountは、ソースコードのコメントも豊富で、デバッグメッセージも詳細だったので、比較的スムーズに原因の特定や対応方法を見い出すことができました。このあたりが不十分なソフトウェアでは、ソースコードの重要そうなポイントにprintf文を追加して進行状況を確認したり、デバッガでステップトレースしながら変数の値を確認するような作業が必要になるでしょう。

Cで書かれたソースコードの場合、構成しているファイルが膨大な数に昇り、それら全てを調べることは不可能なことも多いでしょう。そのような大規模なソフトウェアでは、出力メッセージを手がかりに問題となっているファイルや行を調べ(出力メッセージが不十分ならばソースコードレベルでデバッグメッセージを追加して⁠⁠、そこから処理の流れを遡っていくのが、一見遠回りながら、最も効率的な方法だと思っています。

おすすめ記事

記事・ニュース一覧