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

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

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

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

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

筆者が初めてGNUソフトウェアに触れたころは,まだAutoconfなどの仕組みは用意されておらず,READMEファイルなどに「このOSではこのファイルをこう修正しろ」みたいな説明が列挙されていて,それに従ってヘッダファイルなどを手動で修正してコンパイルしたものでした。READMEには記載がないOS上で使いたい場合などは,システムに用意されているヘッダファイルを元に,どのOSに近いかを調べたり,どのような修正を行えばコンパイルできるかをあれこれ試してみて,苦労の末に動いたときは「やった!!」と思ったものです。

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

後述するようにこのエラーは,カーネルは新しくなっているのにヘッダファイル類がそれに追従していなかったために発生したトラブルで,tar-1.19の問題ではありません。また,現在公開しているPlamo-4.22ではヘッダファイルなどを更新しているために,この問題は発生しなくなっています。

コンパイルエラーの発生

今回取りあげるソフトウェアは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 < 0)
 117 ¦    {
 118 ¦# if HAVE_FUTIMESAT
 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というのは,いったいどこで定義されているシンボルなのでしょう?

ここで使っているfindxargsを組み合わせたコマンドラインは,多数のファイルやディレクトリをまとめてgrepする際の常套手段です。"find/usr/include ¦" で/usr/include 以下にあるファイルの名前がサブディレクトリ以下にあるファイルも含めて1行ずつパイプに出力され,xargsがその行を受けとって,各行に対して "grep AT_FDCWD" コマンドを実行します。

単純にワイルドカードを使って "grep AT_FDCWD /usr/include/*" などとすると,ワイルドカードにマッチするファイルが多すぎてエラーになってしまうことがありますが,find と xargs を組み合わせるとそのようなエラーを回避できます。

著者プロフィール

こじまみつひろ

Plamo Linuxとりまとめ役。もともとは人類学的にハッカー文化を研究しようとしていたのが,いつの間にかミイラ取りがミイラになってOSSを仕事にするようになってしまいました。最近はスペシャリスト養成を目的とした専門職大学院で教壇に立ったりもしています。

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

コメント

コメントの記入