Ko-Window プログラミング、入門編 その5 「デバッグのために」 プログラミングとは切っても切れない間柄のデバッグ作業。それについてもここで 少し触れてみましょう。 ●コンパイルエラーとバグ コンパイル時に出てくるエラーはバグ以前の問題で、いわゆる文法エラーです。こ れは慣れた人なら、メッセージが出た瞬間その原因がひらめき、ささっとソースを修 正して直してしまうことでしょう。エラーメッセージが流れるほど出てくることもあ りますが、いちいち全部追い掛ける必要はありません。たいてい、最初の文法エラー が原因でその後のコンパイルに影響を与えてるだけなので、最初の要因を直してから、 コンパイル、を繰り返せば途端にエラーが減っていくはずです。 ただソースリストが大きかったりすると、何度もコンパイルしたりエディタで編集 したりするのは時間もかかり、結構やっかいです。ところがこの作業は、工夫しだい でかなり効率良くデバッグできるようになります。それが分割コンパイルです。 ●分割コンパイル 分割して1つ1つのソースを小さくすることは、開発に対してさまざまな利点をもた らします。 ・エラー箇所が分かりやすくなる 分割して関連ある部分ごとまとソースにめてあるため、エラー箇所の絞り込 みが容易で修正しやすくなります。またグローバル参照の変数や関数を減ら し、個々に閉じたプログラムを書くことができるため、シンボル衝突を防ぐ と同時に保守性を高める効果もあります。 ・コンパイル時間が短縮される これは分割における最大のメリットの1つです。デバッグによる修正も書き換 えたソースのみ再コンパイルすれば済むので、コンパイル時間は大幅に減少 されます。1番時間のかかるコンパイル処理が、だいたい 1/分割数 で済むわ けですから、どれだけ効果が大きいかわかるでしょう。 ・消費メモリが少なくなる 一度にコンパイルする容量が小さくなるため、コンパイラの消費するメモリ が小さくなります。だいたいフリーエリアが 1.5M あれば、make 上で余裕 でコンパイルできるはずです。メモリの少ないマシンではもちろん、メモリ をかなり増設しているマシンでも、これで浮いたメモリをディスクキャッシュ や RAMDISK に回せば、ますます速いコンパイルができるようになるでしょう。 このように、徹底した分割コンパイルは、開発効率を最大限あげることにつながり ます。 もちろん make は必須のコマンドです。これ無くして分割コンパイルはできません。 gnumake もしくは XC v2 付属の make が使えます。(gnumake を使う場合は消費メ モリが増えるので、メモリのフリーエリアに注意して下さい) 経験上、X68000 では1つのソースは大体 2〜3Kbyte、どんなに大きくなっても 10Kbyte を越えてはいけません。私が今まで書いてきた厖大な量の Ko-Window アプ リケーションが、極めて短期間の間に組まれていることを見れば、分割開発にどれだ け意義があるのかわかるのではないでしょうか。もちろん当初使っていたマシンは 10MHz 無改造の X68000 です。初期の厖大なプログラムは全部マシンで組みました。 ●やっかいなバグとは 本当にやっかいなバグというのは、コンパイルが問題なく通った後にはじめて現れ ます。いきなりバスエラーで止まるもの、起動したものの思い通りに動かないもの、 そして1番難解なものは、ちゃんと動くようにみえるのに何度もやってるとたまに失 敗する、というものです。 デバッグに必要な情報は2つ、 バグのある場所を特定する バグの原因を究明する デバッグの難しさは、この2つがどれだけ絞り込めるかに大きく依存しています。 デバッグにどこから手をつけていいかわからないという場合は、まずこのバグのあ る場所の場所を限定することから手をつけるのが得策です。 ●イベントスタック表示を見る Ko-Window でシステムエラーが発生した場合、エラーウィンドウにその時のイベン トスタックのダンプ表示を行わせることができます。これを見ておくと、だいたいど のイベント発生時に起こったものか、というのを判断することができます。 これは wsrv.x の起動時にオプションで指定します。詳しくは基本セット付属の wsrv.doc を参照して下さい。 wsrv.x -d1 エラー時にイベントスタックの表示を行う wsrv.x -d3 デバッグモードで起動する wsrv.x -d7 トレースモードで起動する ●プログラムが思いどおりに動かない場合、どうやってデバッグをしますか? 私の場合はまず、怪しいと思われるコード部分とじーっと睨めっこします。もちろん 頭の中でプログラムのトレースをしているわけです。 バグの発生しやすい場所は、アドレス(ポインタ)、スタック、ファイル、です。も しバスエラーやアドレスエラーが発生する場合であれば、原因はこの中に収まる可能 性が極めて大です。 それで原因がわからなかった時は、エラー箇所の絞り込み、を行います。少なくと もどこまではプログラムがちゃんと動いたか、またはその時の変数の値でもわかれば、 要因を探る材料にもなります。 コードの節目に printf() 等を徹底して埋め込み、場合によっては変数値を表示さ せながら動かし、場所の特定を行います。ただし、この printf() というのはウィン ドウプログラムでは使えません(標準入出力がないためです)。そこで代わりに Console というのを用います。 ● Console への出力 ウィンドウ画面では、同時に複数のプログラムが動いているためどこに文字列を表 示する、という特定ができません。そこでどれか1つのウィンドウを代表者と決め、 ちょっとしたエラーメッセージ等の文字列表示をすべてそこに行わせることにします。 その代表者を Console といいます。 現在 Console として使えるプログラムは、Command.win と KoConsole.win の2つで す。Command.win は、-tConsole というオプションをつけたウィンドウだけが Console になります。KoConsole.win は Console の出力表示専用です。もちろん、Console は ウィンドウ上に 1つしか存在できません。 Console へ文字列を表示させるのは簡単です。ウィンドウプログラムの好きな場所 に、次のような行を加えてみて下さい。 ConsoleOpen(); ConsolePrint( "Consoleへの文字列表示\r\n" ); Close は必要ありません。また ConsoleOpen() は何度実行しても大丈夫です。とに かく ConsolePrint() の前に1回以上 ConsoleOpen() を書いておけば正しく Console に出力が行われるはずです。 printf() のようなフォーマット付き出力を行う場合は、ConsolePrintf() を使いま す。例えば ConsoleOpen(); ConsolePrintf( "i=%d\r\n", i ); となります。 Console について詳しくは、別ドキュメントで触れたいと思います。ここではデバッ グ時に活用できる、ということで覚えておいて下さい。 ● Ko-Window でひっかかりやすいポイント ありがち用例集 ・fopen() が失敗する fopen() が原因不明で失敗する、などという場合は HEAP エリアの確保をし ていない可能性があります。グローバル変数 WindowHeapSize の設定を行っ ているかどうか再確認してみて下さい。 -> file.doc 参照 ・表示文字列が崩れる DrawSet〜() の関数は、実際の表示は行わずにワークエリアに情報を格納す るだけに過ぎません。表示そのものは WindowDraw() によってはじめて行わ れるため、それを忘れているとうっかり次のようなプログラムを書いてしま うことがあります。 drawset( dbuf, num1, num2 ) DrawBuf *dbuf; { char buf[100]; sprintf( buf, "%d", num1 ); DrawSetSymbol( dbuf, X1, Y1, buf, ATTR, FONT ); sprintf( buf, "%d", num2 ); DrawSetSymbol( dbuf+1, X2, Y2, buf, ATTR, FONT ); } これはどこが悪いかわかりますでしょうか。まずい点は2つあります。 1) 文字列領域 buf が重なっている これを実際に動かすと、num2 の値が2つ表示されてしまうことでしょ う。つまり DrawSetSymbol() は dbuf という変数に座標と一緒に buf のアドレスを書き込んでいるに過ぎません。buf の中身を参照 するのはずっとあとになってからです。だから buf の同一アドレス が2度書き込まれ、2度目の sprintf() で書き換えた内容のみあと から参照されてしまうわけです。 2) auto 変数領域のアドレスを呼び出し元へ返している スタック上に確保された文字列バッファ buf のアドレスを、呼び出 し元に返しています。リターンとともにスタックフレームは消滅し ますので、これではそのスタックに別の値が書き込まれてもおかし くはありません。 というわけで、正しくは(もしこのまま修正するなら)次のようになります。 drawset( dbuf, num1, num2 ) DrawBuf *dbuf; { static char buf1[100], buf2[100]; sprintf( buf, "%d", num1 ); DrawSetSymbol( dbuf, X1, Y1, buf1, ATTR, FONT ); sprintf( buf, "%d", num2 ); DrawSetSymbol( dbuf+1, X2, Y2, buf2, ATTR, FONT ); } -- 1994 9/01 作成 1995 9/15 加筆修正 小笠原博之 oga@dgw.yz.yamagata-u.ac.jp DenDenNET: DEN0006 COR.