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

第8回ソースコードを追いかける

最近のソフトウェアはconfigureスクリプトなどのおかげで、よほどのことが無い限りコンパイルエラーは発生しなくなり、「なぜコンパイルエラーが出るのか」⁠どう修正すればコンパイルが通るのか」をソースコードレベルで調べる機会はほとんど無くなりました。

もちろん「コンパイルすらできないソースコードを公開した」というのは(スナップショット版ではいざ知らず)開発者にとっては最大級の恥辱なので、不完全なソフトウェアの公開を勧めるつもりは毛頭もありませんが、自らの経験を振り返ると、⁠コンパイルするためにソースコードを調べて修正する」というのは、ソースコードを読むいい勉強になったように思います。

今回とりあげるのは、最近では珍しいconfigureスクリプトは通ったのにコンパイルできなかったソフトウェア」の例で、実際にそのような状況になった際、何をどのように調べて原因や対策を考えたかの実例を紹介しようと思います。具体的には、Plamo Linuxの開発版環境でtar-1.19をコンパイルしてエラーになった際の例です。

コンパイルエラーの発生

今回取りあげるソフトウェアはGNU tar-1.19です。tarは、もともとTapeARchiverから名付けられたソフトウェアで、HDD上の複数のファイルを磁気テープにまとめてバックアップするために作成されました。現在では磁気テープへのバックアップのみならず、複数のファイルを1つにまとめるアーカイバとして広く利用されています。

先に公開したPlamo Linux 4.22ではtar-1.16というバージョンを採用していましたが、メンテナの一人が「tar-1.16では4Gバイトに迫るような大規模なアーカイブを作ろうとするとエラーになるが、新しい1.19では問題なく作成できた」旨の報告をしてくれたので、手元でも新しいバージョンを試してみたくなりました。

tarのような基本ソフトウェアが正常に動作しなくなると日常作業にも支障が出ます。そこで、tar-1.19のテストにはVMware上に構築した仮想環境を使うことにしました。その際、たまたま手元にあったのがPlamo-4.22β1という正式リリースの少し前のバージョンをインストールした環境でした。「β1」というバージョンは、大きな改修はほぼ終了して、リリースに向けたテスト段階に入っていることを示します。そこで「公開版と大差ないはず」と考えて、この環境でtar-1.19をコンパイルしようとしたところ、謎なエラーが発生しました。

configureスクリプトを実行する前にREADME等のファイルに目を通したのは前回紹介したGIMPの例と同様ですが、tar-1.19では configure スクリプトは正常に終了したのに、コンパイルしようとするとコンパイルエラーが発生します。

 kojima@vmathlon/tar-1.19]% ./configure
 checking for a BSD-compatible install... /usr/bin/install -c
 checking whether build environment is sane... yes
 checking for gawk... gawk
 ...
 config.status: creating po/Makefile
 config.status: executing tests/atconfig commands
 
 kojima@vmathlon/tar-1.19]% make
 make[1]: Entering directory `/home/kojima/tar-1.19'
 Making all in doc
 make[2]: Entering directory `/home/kojima/tar-1.19/doc'
 ...
 depbase=`echo utimens.o ¦ sed 's¦[^/]*$¦.deps/&¦;s¦\.o$¦¦'`; \
 if gcc -std=gnu99 -DHAVE_CONFIG_H -I. -I. -I..     -g -O2 -MT utimens.o -MD -MP -MF "$depbase.Tpo" -c -o\
  utimens.o utimens.c; \
 then mv -f "$depbase.Tpo" "$depbase.Po"; else rm -f "$depbase.Tpo"; exit 1; fi
 utimens.c: In function `gl_futimens':
 utimens.c:119: warning: implicit declaration of function `futimesat'
 utimens.c:119: error: `AT_FDCWD' undeclared (first use in this function)
 utimens.c:119: error: (Each undeclared identifier is reported only once
 utimens.c:119: error: for each function it appears in.)
 make[3]: *** [utimens.o] Error 1
 make[3]: Leaving directory `/home/kojima/tar-1.19/lib'
 ...
 make: *** [all] Error 2

あれれ、tarのコンパイルに失敗したことなんて今までに無かったのになぁ…、と首をひねりつつ、エラーの原因を調べてみることにしました。

エラー原因の調査

先のエラーメッセージを見ると、エラーになったのはutimens.cというファイルの119行めで、AT_FDCWDというシンボルが定義されていないことが原因のようです。そこで該当箇所を調べてみました。

makeはMakefileの指示に従って複数のディレクトリを再帰的に処理していくので、まずはutimens.cというファイルがどこにあるかを確認する必要があります。makeの出力したメッセージをチェックすると、このファイルを処理している時はtar-1.19/lib/で作業していたことがわかりますので lib/utimens.c というファイルを開いて、その119行目を調べてみました。

該当箇所はこんな感じでした。

 116 ¦  if (fd 119 ¦      return futimesat (AT_FDCWD, file, t);
 120 ¦# endif
 121 ¦    }

確かに119行目にAT_FDCWDという見なれないシンボルがあります。このシンボルが未定義(undeclared)になっていることがコンパイルエラーの原因なのは分りましたが、なぜ未定義のシンボルがここで使われているのでしょう。他にこのシンボルが使われている部分が無いかと、grepで調べてみました。

 kojima@vmathlon/tar-1.19/lib]% grep AT_FDCWD *
 at-func.c:  if (fd == AT_FDCWD ¦¦ IS_ABSOLUTE_FILE_NAME (file))
 chdir-long.c:  cdb->fd = AT_FDCWD;
 ...
 getcwd.c:# undef AT_FDCWD
 getcwd.c:# define AT_FDCWD (-3041965)
 getcwd.c:#ifdef AT_FDCWD
 getcwd.c:  int fd = AT_FDCWD;
 getcwd.c:     AT_FDCWD is not defined, the algorithm below is O(N**2) and this
 ...
 openat.h:#ifndef AT_FDCWD
 openat.h:# define AT_FDCWD (-3041965)
 utimens.c:      return futimesat (AT_FDCWD, file, t);

grepで他のソースコードを調べるとgetcwd.cやopenat.hでAT_FDCWDが使われていることがわかりましたし、一部ではdefine文でこのシンボルを定義し直しているようです。そこで、getcwd.cの該当箇所を調べてみました。

 57 ¦/* Work around a bug in Solaris 9 and 10: AT_FDCWD is positive.  Its
 58 ¦   value exceeds INT_MAX, so its use as an int doesn't conform to the
 59 ¦   C standard, and GCC and Sun C complain in some cases.  */
 60 ¦#if 0 < AT_FDCWD && AT_FDCWD == 0xffd19553
 61 ¦# undef AT_FDCWD
 62 ¦# define AT_FDCWD (-3041965)
 63 ¦#endif

ソースコードに付いたコメントを見ると、Solaris のいくつかのバージョンではAT_FDCWDがINT_MAXを越える正の値になっていることがあって、コンパイラが文句を言うことがあるからその修正を行なっている、ということのようです。具体的には、AT_FDCWDが0以上で0xffd19553と等しい場合にAT_FDCWDを-3041965に再定義する、というコードのようですが、このコードが機能するためにはAT_FDCWDが未定義ではダメなはずです。

それではtar-1.19のソースコードのどこか別の部分でこのシンボルを定義しているのかな、と調べてみました。

 kojima@vmathlon/tar-1.19]% find . -name "*.[ch]" ¦ xargs grep AT_FDCWD
 ./lib/chdir-long.c:  cdb->fd = AT_FDCWD;
 ./lib/chdir-long.c:/* Given a file descriptor of an open directory (or AT_FDCWD), CDB->fd,
 ./lib/getcwd.c:#include <fcntl.h> /* For AT_FDCWD on Solaris 9.  */
 ...
 ./lib/utimens.c:      return futimesat (AT_FDCWD, file, t);

結果を見ると、AT_FDCWDというシンボルはlib/以下でしか使われていないようです。先にgetcwd.cの該当箇所を調べたように、lib/以下のコードはAT_FDCWDは定義済みという前提で書かれているので、AT_FDCWDは、どうやらtar-1.19の外部で定義されているべきシンボルのようです。そこでシステム側の定義を調べてみることにしました。

UNIXの世界では、Cで書かれたソフトウェアが利用するヘッダファイルは/usr/include/ 以下に置くことになっています。そこで、/usr/include/ 以下でAT_FDCWDというシンボルがどのように使われているかを調べてみました。

 kojima@vmathlon/tar-1.19]% find /usr/include ¦ xargs grep AT_FDCWD
 kojima@vmathlon/tar-1.19]%

あれれ、何も見つかりません。tar-1.19の中にも、システムのヘッダファイルにも無いとすると、AT_FDCWDというのは、いったいどこで定義されているシンボルなのでしょう?

設定ファイルの確認

lib/utimens.cの中でAT_FDCWDが使われているのは# if HAVE_FUTIMESAT ~#endifで括られた中でした。すなわち、HAVE_FUTIMESATというシンボルが定義されていなければこの部分は通らないのでエラーにはならないはずです。そこでHAVE_FUTIMESATというシンボルはどこで定義されているかを調べてみました。

 kojima@vmathlon/tar-1.19]% find . ¦ xargs grep HAVE_FUTIMESAT
 ./lib/utimens.c:#if HAVE_FUTIMESAT ¦¦ HAVE_WORKING_UTIMES
 ...
 ./config.log:¦ #define HAVE_FUTIMESAT 1
 ./config.log:#define HAVE_FUTIMESAT 1
 ./config.h:#define HAVE_FUTIMESAT 1

HAVE_FUTIMESATはconfig.hの中で 1 に定義されているようです。config.hはconfigureスクリプトによって生成されるファイルなので、configureスクリプトが「このシステムにはfutimesatという機能がある」と判断したことになります。そこでfutimesatをキーワードにconfigureスクリプトのログconfig.logを調べてみたところ、このような部分がありました。

 configure:9225: checking for futimes
 configure:9281: gcc -std=gnu99 -o conftest -g -O2   conftest.c  >&5
 configure:9287: $? = 0
 configure:9305: result: yes
 configure:9225: checking for futimesat
 configure:9281: gcc -std=gnu99 -o conftest -g -O2   conftest.c  >&5
 configure:9287: $? = 0
 configure:9305: result: yes

この結果を見ると、configureスクリプトがテスト用のCプログラムをコンパイルしてみて大丈夫だったから、futime, futimesatが使えることを示すHAVE_FUTIMESATやHAVE_FUTIMESといったシンボルが#defineされたのだろうことがわかります。

それでは、futimesatやfutimesはシステム的にどう定義されているのでしょう?

 kojima@vmathlon/tar-1.19]% find /usr/include ¦ xargs grep futimesat
 find /usr/include ¦ xargs grep futimesat
 kojima@vmathlon/tar-1.19]% find /usr/include ¦ xargs grep futimes
 find /usr/include ¦ xargs grep futimes
 /usr/include/sys/time.h:extern int futimes (int __fd, __const struct timeval __tvp[2]) __THROW;

この結果を見ると、futimesは/usr/include/sys/time.hで定義されているもののextern定義になっているので、実態はどこか別にあるはずです。またfutimesatは該当する定義が見つかりません。

これを見る限り、この環境でconfig.hにHAVE_FUTIMESATやHAVE_FUTIMESという定義が行なわれることがおかしいようです。そこで、HAVE_FUTIMESの定義部分をコメントアウトして、再コンパイルを試してみました。

config.hのコメントアウト部分
 /* Define to 1 if you have the `futimes' function. */
 /* #define HAVE_FUTIMES 1 */
 kojima@vmathlon/tar-1.19]% make
 make
 make  all-recursive
 ...
 make[2]: Leaving directory `/home/kojima/tar-1.19'
 make[1]: Leaving directory `/home/kojima/tar-1.19'
 kojima@vmathlon/tar-1.19]%

今度は無事tar-1.19のコンパイルが終了しました。どうやら、この環境(Plamo-4.22β1)では、本来は存在しないfutimesやfutimesatという関数が、configureスクリプトには存在すると誤認され、不適切な設定ファイル(config.h)が生成されたことによるエラーだったようです。

あまり綺麗な方法ではありませんが、この程度の修正ならばconfigureスクリプトを実行してから、config.hファイルをsed等で修正するような処理を追加すればパッケージ化することも可能でしょう。

その後の展開

とりあえずPlamo-4.22β1の環境でtar-1.19をコンパイルする方法は見つかったのですが、config.hを修正するというのはあまり綺麗な方法ではないので、改めて正式公開版のPlamo-4.22の環境でどうなるかを試してみたところ、config.hを修正する必要もなくconfigure && make でコンパイルが完了してしまいました。

あれれ、と思いつつPlamo-4.22環境でAT_FDCWDというシンボルが存在するかを調べると、

 kojima@xeon]% find /usr/include ¦ xargs grep AT_FDCWD
 /usr/include/linux/fcntl.h:#define AT_FDCWD             -100    /* Special value used to indicate

となり、/usr/include/linux/以下に収められたカーネルのヘッダファイルの中でAT_FDCWDという定数が設定されているようです。そこで、Plamo-4.22β1と4.22の間でカーネルのヘッダファイルがどう変ったのかを調べると、4.22β1ではglibc2をコンパイルした環境であるLinux 2.6.14由来のヘッダファイルを使っていたのに対し、4.22ではインストールするカーネルと同じ2.6.22.9由来のヘッダファイルを使っていました。すなわち、2.6.14 から2.6.22.9の間のどこかで、カーネルヘッダにAT_FDCWDという定数が設定されるようになり、その結果、両者の間で動作の違いが生じていたようです。

/usr/include/linux/以下に収めるカーネルのヘッダファイルをどのように用意するかはさまざまな流儀があります。昔のLinuxでは/usr/src/linux/以下にソースコードを展開し、/usr/include/linuxは/usr/src/linux/includeへのシンボリックリンクにする、という形でソースコードにあるヘッダファイルをそのまま利用する、という方法が取られていました。

しかし、カーネルのソースコードが急速に変化していくのに対して、システムのCライブラリ(glibc2)の変化は緩慢なため、カーネルのヘッダファイルを直接利用すると、Cライブラリとの間に齟齬が生じるという問題が指摘され、Cライブラリをビルドした際のカーネルヘッダファイルを別途用意しておくようなスタイルに移行したことがあります。Plamo-4.22β1では、そのようなスタイルを採用していたので、カーネルのヘッダファイルはglibc-2.3.6をコンパイルした2.6.14カーネルの環境を利用していました。

しかしながらこの方法では、カーネルに新しい機能が組み込まれても、それに対応したバージョンのCライブラリが用意されるまではその機能を利用できない、という不便さが指摘され、最近ではカーネルのソースコードのうち、ユーザ空間から利用してもいいヘッダファイルのみを取り出すmake headers_installという機能がカーネルのMakefileに用意され、/usr/include/linux/等にはこのコマンドで取り出されたヘッダファイルを配置するような流儀になっています。Plamo-4.22では、このスタイルを採用してカーネルのヘッダファイルのパッケージを更新したおかげで、AT_FDCWDという定数がユーザ領域のソフトウェア(tar-1.19)から利用できるようになっていた、というのが両者の違いだったようです。

この結果を踏まえると、β1環境での修正も、HAVE_FUTIMESをコメントアウトして機能を削除するよりも、AT_FDCWDを-100に定義するような修正を追加する方がいいでしょう。ただ、そもそもβ1環境というのは開発の途中経過であり、現在では問題自体が存在しなくなっているので、この件については、この連載の中に記録するだけに留めることにしました。

おすすめ記事

記事・ニュース一覧