ここでは、わたしがさまざまなプログラミングをしている中で失敗した事柄を紹介していきます。
どうか、これを読んでいる方々が同じ轍を踏みませんように。
Windows や Linux ではちゃんと動作する (ように思われる) のに、FreeBSD へ持っていくと突然コアダンプをするようになってしまったプログラムがありました。与えられた文章を構文解析して文に対する確率を計算する、というプログラムだったのですが、ある特定のデータ以外ならばちゃんと計算することができ、その計算結果も正しいものになります。特定のデータに対してのみ、コアダンプしてしまうのです。
さて、これはいったいどういうことなのでしょうか?
最初は『FreeBSD のカーネルにバグ (もしくはメモリを扱う上での癖など) があるのでは?』と思ったのですが、そう簡単に人のせい、しかもハッカーというものすごい方々のせいにしてしまうのは恐れ多いことです。そんなわけで、printf
を使ってコアダンプの発生している部分をせっせと探します。
(どうして gdb を使わない? というと、単にわたしが使い方を 知らなかったから です……(^^;)
printf
をいろいろなところに埋め込んで、少しずつ問題の場所を特定していきます。printf
を入れてもコアダンプが止まるわけでもありませんから、何回も何回も 60MB 近くあるコアダンプが生成されることに。かなり間抜けなことをしていたと思います。ちなみに、この作業は FreeBSD 上で行っていたためコアダンプが発生しても OS が落ちるようなことがありませんでした。嗚呼、Win9x でなくて良かった……。
さて、ひたすら、printf
を埋め込んで実行して……を繰り返し、ついに問題を巻き起こしているソース中の行を特定することができました。その行には comp
と prev
、そして item
という三つの変数が使われています。すべてポインタですから、おそらく、
printf
で…)。すると、それぞれ他のデータと似通ったようなアドレスになっており、とくに際だった問題があるようには感じられません。偶然にも似通ったアドレスになっている、という可能性もありますが、それだったら他のデータでももっと激しくコアダンプするはずです。ということで、この可能性はとりあえず除外しておきます。printf
を入れ、止まるかどうかを確かめる、と。item
というポインタの指している領域がおかしい』ということが判明しました。このポインタ、キューからもらってきたポインタです。そのキューというのがちょっとトリッキーなことをしていて、返すデータの実体をキューとして確保しているメモリの最後尾に置いておき、そこへのポインタを返すという仕組みになっているのです。item
ですが、よ〜くソースを見ると、その直前にある if ブロックの内部で、キューへのプッシュが行われています。item
は移動前のキューを参照していたのでしょうが、新しいデータがキューにプッシュされたときにキューの拡張が行われ、同時にメモリも移動したとすれば……、すべてが解決します。 Java には、あるクラスが他のクラスを import しようとしたとき、ソースファイルのタイムスタンプがクラスファイルのものより新しくなっていると自動的にそれをコンパイルしてくれるという機能があります。また、import しようとしたクラスファイルが存在しなかったときも同様のことを行ってくれます。
では、クラス A とクラス B がお互いに import しているときはどうなるでしょうか。クラス A は B.class を検索し、存在しなかった場合は B.java を探してそれをコンパイルするようにします。しかし B.java は A.class を必要としていて、A.class を作るためには A.java をコンパイルしなければなりません。ところがところが、A.java をコンパイルするには B.class が必要になって……と、ぐるぐる回り始めてしまうような気がしますよね。
実際にはそんなことなどなく、ちゃんとコンパイルできます。
しかし、package を指定したときは、若干話が変わってくるのです。今回の失敗は、それが原因でした。
package x を指定して作成したクラス A と B があったとします。A.java では import x.B;
とあり、B.java には import x.A;
とあります。ではこれをコンパイルしてみましょう〜。まず A.java をコンパイルすると、import のところで x/B.class が検索されます。しかしそんなクラスは存在しないので……あれ? クラスファイルがカレントディレクトリに置かれる……。
というわけで、まず第一ポイント。「package を使ったときは javac に -d オプションを忘れずに」
出力先ディレクトリを指定したから、今度は x/B.class も作られるぞ……と思いきや、今度は「x/B.class が見つからない」というエラーが。おいおい、それはあんたがコンパイルしてくれるのと違うのん? ということで仕方なく B.java をコンパイル。すると「x/A.class が見つからない」というエラーに。おいおいおいおい〜、それを作れないからこっちをコンパイルしとるんじゃボケぇー!
はてさて、これはどうしたものかと、いろいろなドキュメントを手繰ってみます。すると「ソースパスの指定が必要」というところを発見。なるほど、これを指定すればいいのか〜、ということで -sourcepath を指定。
javac -sourcepath .. -classpath .. -d ..
とする。