Technique Box

負例からの学習

SOFTWARE] - [ルートメニュー



 ここでは、わたしがさまざまなプログラミングをしている中で失敗した事柄を紹介していきます。
 どうか、これを読んでいる方々が同じ轍を踏みませんように。

  1. OS 依存のコアダンプ
  2. Java でクラスの相互参照ができない!?

OS 依存のコアダンプ

 Windows や Linux ではちゃんと動作する (ように思われる) のに、FreeBSD へ持っていくと突然コアダンプをするようになってしまったプログラムがありました。与えられた文章を構文解析して文に対する確率を計算する、というプログラムだったのですが、ある特定のデータ以外ならばちゃんと計算することができ、その計算結果も正しいものになります。特定のデータに対してのみ、コアダンプしてしまうのです。
 さて、これはいったいどういうことなのでしょうか?

 最初は『FreeBSD のカーネルにバグ (もしくはメモリを扱う上での癖など) があるのでは?』と思ったのですが、そう簡単に人のせい、しかもハッカーというものすごい方々のせいにしてしまうのは恐れ多いことです。そんなわけで、printf を使ってコアダンプの発生している部分をせっせと探します。
(どうして gdb を使わない? というと、単にわたしが使い方を 知らなかったから です……(^^;)
 printf をいろいろなところに埋め込んで、少しずつ問題の場所を特定していきます。printf を入れてもコアダンプが止まるわけでもありませんから、何回も何回も 60MB 近くあるコアダンプが生成されることに。かなり間抜けなことをしていたと思います。ちなみに、この作業は FreeBSD 上で行っていたためコアダンプが発生しても OS が落ちるようなことがありませんでした。嗚呼、Win9x でなくて良かった……。
 さて、ひたすら、printf を埋め込んで実行して……を繰り返し、ついに問題を巻き起こしているソース中の行を特定することができました。その行には compprev、そして item という三つの変数が使われています。すべてポインタですから、おそらく、

というような理由のために、コアダンプしてしまっていたのでしょう。

 まずは、関係ない部分を示していないか、ポインタ値を表示させて確認してみます (また printf で…)。すると、それぞれ他のデータと似通ったようなアドレスになっており、とくに際だった問題があるようには感じられません。偶然にも似通ったアドレスになっている、という可能性もありますが、それだったら他のデータでももっと激しくコアダンプするはずです。ということで、この可能性はとりあえず除外しておきます。
 次は、指しているメモリが有効なものであるかどうかです。が、これもまた同じ理由で可能性を除外しました。めんどくさくなってき ポインタの付け替えやメモリの確保、解放に欠陥があると思えなかったというのもあります。
 って、ちょっと待った。それではいったい、何が原因だというのでしょうか? そこで、事態をさらに詳細に調べるため、上に示した三つの変数のうち、いったいどれがおかしくなっているのかというのを特定します。これは簡単で、参照先の値を自分に書き戻すコードを追加するだけです。さらに、その前後に printf を入れ、止まるかどうかを確かめる、と。
 ……繰り返しますが、gdb の使い方がわかっていれば、こんなことはしません よ(笑)。デバッガ上でできることでしょうしね。いちおう、使ってもみたのですが、いまいちうまくいかなくて。
 さて、この方法によって問題を特定すると『item というポインタの指している領域がおかしい』ということが判明しました。このポインタ、キューからもらってきたポインタです。そのキューというのがちょっとトリッキーなことをしていて、返すデータの実体をキューとして確保しているメモリの最後尾に置いておき、そこへのポインタを返すという仕組みになっているのです。
 つまり、キューとして確保されているメモリが[A, B, C, *, *](* は空) となっていたら、キューをシフトして値を取り出すとバッファの内部は[B, C, *, *, A]となり、最後尾の A へのポインタが返されるというわけです。
 さて、この item ですが、よ〜くソースを見ると、その直前にある if ブロックの内部で、キューへのプッシュが行われています。
 ……。
これか!!
 あはは……。なんという事はなく、移動してしまった後のメモリを参照していたのです。それじゃ、確かに動かんわな。
 キューへデータをプッシュしたとき、キューのサイズが足りなくなってしまうことがあります。そのときは仕方がないので realloc することで対処するのですが、realloc はメモリが移動するという可能性を潜ませています。item は移動前のキューを参照していたのでしょうが、新しいデータがキューにプッシュされたときにキューの拡張が行われ、同時にメモリも移動したとすれば……、すべてが解決します。
 そこで、キューのシフトにおいてポインタを受け渡しするのではなく、ポインタの示す先のデータ (構造体でした) を直接コピーするように変更を加えてみると、まるで何事もなかったかのようにスイスイ動作してくれました。やはり、メモリの移動が原因だったようです。
 では、どうして Linux などではうまく動いていたのでしょうか? それは、OS の確保するメモリ領域サイズが異なるためだと考えられます。Windows などでは多めにメモリを確保しているため、realloc が発生してもメモリが移動せず、結果としてポインタは正当なものとなっていたのでしょう。
 FreeBSD は厳格なメモリ管理を行っているのかもしれませんね。


Java でクラスの相互参照ができない!?

 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 を指定。

「x/B.class が見つからない」
… … …
ウキャー!
 さらにドキュメントを調べてみると、「ソースの検索位置 = ソースパス + パッケージ名」(クラスファイルも同様) という規則があることを発見。つまり? ソースファイルは ./x に、クラスファイルも ./x に置けばいいということか? で、./x/A.java などが検索されればいいから……、えーと、こうすればいいのかな? まず、./x/A.java と ./x/B.java を作成。で、コンパイル時はカレントディレクトリを ./x にして、javac -sourcepath .. -classpath .. -d .. とする。
 どうだ!?
 おおっ、成功!
 というわけで、ようやく package 内でのクラス相互参照ができるようになりました。第二ポイント。「ソースの検索位置 = -sourcepath の引数 + パッケージ名」
 今回の教訓。便利なことをやってくれるソフトウェアを使うときには、それが「どんな仕組みで動いているのか?」というのを知っておくようにしましょうね。何かが起こったときに困ってしまいますから。




SOFTWARE] - [ルートメニュー