デバッグの題材:「型変換の不具合」

前回に引き続き、今回のバージョンアップ(リリースバージョン1.5321)で関わった部分を具体例にあげながら、 Turbo Delphi でプログラムのデバッグをする方法を紹介します。例として使うのは、次の修正です:

  • 命令の引数の型を変換する際の不具合を修正 (r189)

バグ報告板では、チケット @357 に該当します。他の命令の場合、数値を渡すべきところに文字列を渡すと適切に数値変換されるのですが、なぜかこの命令だけ違反エラーが発生します。これはどうも文法のコアレベルで何か問題がありそうですよ・・・?

まずは検索

では、早速見ていきましょう。前回同様、まずは「乱数初期化」命令を探します。困ったらまずは検索!

function sys_randomize(args: THiArray): PHiValue;
var
  a: PHiValue;
begin
  // (1) 引数の取得
  a := args.FindKey(token_a); // 値
  // (2) データの処理
  if a = nil then
  begin
    RandomMT.Randomize;
  end else
  begin
    RandomMT.Randomize(hi_int(a));
  end;
  // (3) 戻り値を設定
  Result := nil;
end;

これが「乱数初期化」命令の実際の中身です。変な所は特にありませんねぇ。

ブレークポイント

ということで、何がおかしいのか分からないので、「ブレークポイント」を設定してデバッグしてみます。ブレークポイントを設定してプログラムを実行すると、プログラムがブレークポイントまで達した時に一時停止して、その時の変数の中身などを調べることができます。また、そこからステップ実行(1行ずつ実行)なども可能です。

ソースコードエディタで、ブレークポイントを設定する。

図のように、エディタの行番号のところをクリックすると、左に赤い丸が現れます。これがブレークポイントです。(右クリックしてコンテクストメニューから、さらに細かい条件を設定することもできます。)

DLL のデバッグ

では、ブレークポイントを設定したのでデバッグしてみましょう。…と言っても、よくよく考えると、今調べているファイルは dnako.dll の一部(ユニット)です。 DLL とは Dynamic Link Library の略で、他のプログラムから呼び出して使う汎用プログラム部品です。ということは、この DLL を呼び出すプログラムがないとそもそも実行できません

ということで実行の設定を見てみます。プロジェクトマネージャで今開いているプロジェクトが dnako.dll であることを確認して、メニューを「実行 - 実行時引数」と辿ります。(あるいは、「Shift + Ctrl + F11」でプロジェクトのオプションを開いて、「デバッガ」のツリーを開いても同じ設定画面が出ます。)

デバッグ関連の設定を行うデバッガオプション画面

デフォルトでは、ホストアプリケーションが vnako.exe で、パラメータが test.nako になっているはずです。これはつまり、 DLL を呼び出すプログラムとして vnako.exe を指定し、その vnako.exe を実行する時の引数(つまり vnako 方式で実行するなでしこのプログラム)を test.nako にするということです。

vnako.exe は、コンパイル済みならば既に存在するはずです。あとは適当なデバッグ用のなでしこソースを書いて、作業フォルダに test.nako として保存するなどして正しくプログラムが実行されるようにします。(コンパイルの仕方は、コミッタ日記 08/05 「開発環境のインストール〜コンパイル」の「なでしこのコンパイル」を参考にしてください。)

ここでは、 test.nako の内容を単純に次のものにします:

`A`で乱数初期化。

ステップ実行

準備が整ったので、実行してみます。メニューで「実行 - 実行」または「F9」です。

すると、下のログペインに大量のイベントログが流れていきます。しばらく時間がたつと、先ほど設定したブレークポイントに到達します。

// (1) 引数の取得
a := args.FindKey(token_a); // 値

この段階では、この行はまだ実行されていません。(ちなみにこの行は、なでしこ側の引数「A」を取得する処理です。 AS などのよく使われる引数名には、トークンIDが特別に振られていて、名前指定で引数を取得できるようになっています。)

そこで、この1行だけを実行してみます。メニューから「実行 - ステップ実行」もしくは「F8」です。

ステップ実行を一回実行して、次の行の実行待ちになった状態。

すると、緑色の矢印が次の行に現れました。先ほどの行だけが実行されて、次の行の実行待ちに移ったという訳です。

ちなみに、1行ずつコードを実行する「ステップ実行」以外にも、より細かく実行内容(関数などの中身)を見ていく「トレース実行」(ステップインとも呼ぶ)や、現在のブロックを抜け出すまで実行する「呼び出し元に戻るまで実行」(ステップアウトとも呼ぶ)などがあります。状況に応じて使い分けましょう。

ローカル変数を調べる

では、1行実行したので変数の内容をチェックしてみます。

 sys_randomize 関数の現在のローカル変数の値を詳しく見るローカル変数ペイン。

うーん、これでは原因は分からないですねぇ。ひとまずこの内容をメモって(手軽にスクリーンショットでも)先に実行していきます。すると、次の行でエラーが発生しました:

RandomMT.Randomize(hi_int(a));

より細かく見ていくトレース実行を実行すると、 hi_int 関数の、文字列から数値への変換の手前、 hi_str 関数のところでエラーが発生していることが分かります。おかしいですね。他の命令では数値変換は上手く行くのに、なぜこのときだけエラーが出るのでしょう。

プログラムの対称実験

ということで、変換が上手く行く時と比べてみます。文字列「A」を整数変換させるなでしこのプログラムを書いて、整数変換時の変数の内容を調べるのです。

なでしこの「整数変換」命令を検索して、 sys_toInt 関数が見つかるので、その hi_int 関数を実行するところにブレークポイントを設定して実行します。もちろん、なでしこのプログラム test.nako は予め『`A`を整数変換。』にしておきます。

 sys_toInt 関数でブレークしたときのローカル変数ペイン。

これで、変換対象の s の中身はこのようなデータになっていることが分かりました。違う命令から呼び出しているという点以外では、どちらもなでしこ側で文字列リテラル`A`を引数に指定し、内部で hi_int を呼び出しているので、条件は同じはずです。これと、メモってある変数 a を比べます。

すると、 a では下の方が nil だらけだったのに、 s では何かの値が詰まっています。特に、 ptr_s の値は 'A' で、変換前の文字列がちゃんと入っているようです。この差が原因と見て間違いなさそうです。念のため実行を続けると、整数変換は問題なく実行できました。

まとめ

このように、プログラムを途中で止めるブレークポイントと、一行ずつ実行できるステップ実行、そしてその時の変数の値を見るローカル変数ペインを活用することが、デバッグの基本です。上達したら、「カーソル行まで実行」や、値を書き換えることのできる「インスペクト」なども活用できるとさらに良しです。

長い追跡の果てに、エラーの原因は分かりました。しかし、なぜ乱数初期化命令に文字列を与えた時だけ「 nil ぽ!」が発生しているのでしょう。そしてその差はどこから来ているのでしょう。この本質的な原因を突き止めないと、バグの修正はできません。

ということで、長くなったので一旦ここで切って、内容は次回「〜バグ修正編〜」に分けようと思います。