川合のプログラミング言語自作のためのテキスト第四版#001
(1) デバッグレベル
- acl4v2 には「デバッグレベル」という概念があります。
- このデバッグレベルは、コンパイラの最適化レベルのことだろうと誤解されることがあるのですが、それは違います。・・・紛らわしいのは「デバッグ時は最適化レベルを最低にしましょう」という正しいアドバイスがあって、それでデバッグと最適化レベルが結びついているせいだと思います。
- コンパイラの最適化は、生成する機械語の質を上げるためなら、プログラムに書かれた通りには処理しないで、あえて順序を変えたり共通化部分をまとめたりします(でも実行結果は変わりません)。こういうことをされると、変数〇〇に3が代入されるところからトレースしようとか思っても、デバッグがうまくいかないのです。コンパイラはその変数の更新タイミングを保持しなければいけないとは気づけないので、順序入れ替えなどをやってしまっているせいです。・・・だからデバッグ時は最適化をできるだけしないようにするわけです。・・・なお、printfでデバッグをする場合は、printf結果が変わってしまうと実行結果を変えたことになってしまうので、最適化レベルがmaxであっても支障はありません(最適化レベルが高いとコンパイルに時間がかかるという問題はありますが)。
- acl4v2 のデバッグレベルは最適化とは関係なくて、「このプログラムはまだバグがあるかもしれないから、とにかく用心深くチェックしながら実行してね」という意味です。つまりデバッグレベルが5のときでも、最適化レベルを最高にして構いませんし、デバッグレベルが0のときに最適化レベルを最低にしてもかまいません。速いか遅いかというよりも、バグチェックをしながら実行するかどうかの違いです(まあチェックをすれば基本的には少し遅くなりますが)。
- デバッグレベルによるチェック強化は、すべて実行時チェックによるものなので、コンパイル時に追加でエラーが見つかるということはありません(残念ながら)。
- デバッグレベルは a_DbgLv をdefineして指定します(ライブラリのincludeより前に)。デフォルトでは 2 になります。
- a_DbgLv=0 : リリース用のモードで、ライブラリ側でのエラーチェックは一切やらなくなります。ただし、プログラムのバグではなく環境によっておこるエラー(メモリ確保の失敗、ファイルオープン時にファイルが見つからないなど)はチェックします。
- a_DbgLv=1 : エラーチェックはa_DbgLv=0と同等にまで省略されていますが、構造体の中にはエラーチェックのための情報を入れるスペースを確保します。これは「デバッグレベル2だと生じないのに、デバッグレベル0だと発生するバグ」なんて言う厄介な問題が出たときに、問題の切り分けを支援するために用意されているもので、普通はこのレベルを使うことはありません。
- a_DbgLv=2 : 処理速度があまり落ちないようなエラーチェックはすべてやります(デフォルト)。
- a_DbgLv=3 : レベル2とレベル5の間です。
- a_DbgLv=4 : レベル2とレベル5の間です。
- a_DbgLv=5 : どれだけ遅くなってもいいので、今実装されているエラーチェックは全部有効にします。
- acl4v2では、このデバッグレベルの考え方を重視しています。理由はこうです。・・・デバッグレベルがあるので、どれだけエラーチェックをしてもリリース時に実行速度の低下はありません。「こんなチェックをしたら確かに強力だけど、遅くて使い物にならない」みたいなチェックアルゴリズムがあった場合に、ためらうことなくエラーチェックのコードをいれることができます。必要なことはエラーチェックのための記述をためらうことではなく、どのデバッグレベル以上で有効になるのか正しく設定することだけです。
- 私自身は「まあこんなにチェックしても、自分がこのチェックで助かることはめったにないだろう」なんてうぬぼれていたのですが、これを書いて数週間後には、チェックがあるとわかっているので「バグ探しはライブラリにやってもらえばいいやー」と適当に書くようになって、結果的にのびのびとプログラムを作れるようになりました。気分はいいです。おかげで新規にコードを書くときは1日に1回くらいはこのエラーチェック機能に助けられています。
- とまあ概念的なことばかり説明していてもよくわからないと思うので、後で実際のコードを使いながら説明します。
(2) a_malloc(), a_free(), a_realloc()
- これからエラーチェック機能付きのmalloc/free/reallocを作ります。要件はこんな感じです。
- [1]a_DbgLv=0にした場合、標準関数のmalloc/free/reallocにメモリ確保失敗チェックを付けた程度のものになるようにします(a_NoUse_OutOfMemoryCheck!=0のときはメモリ確保失敗チェックすらしません)。
- [2]a_DbgLv=2以上の場合は以下の機能を持ちます:
- [3]メモリを確保した場合に、その中身を 0x87 で塗りつぶします。これは「ここは未初期化です」というマークのつもりです。ゼロクリアとかだと、未初期化のまま進んでも何もトラブルが起きなくて気づけないことがよくあるのですが、 0x87 で埋め尽くしているところを未初期参照して値を使おうとすると、たいてい派手にコケてくれるので気づきやすいです。a_realloc()で容量を広げた場合も、広げた部分は 0x87 で埋めます。 0x87 は他の用途ではめったに出てこない値(かつコケやすい値)なのでこれを採用しました。
- [4]メモリを開放する場合にも、あえて中身を0x87で塗りつぶしてからfree()します。これはたまにミスって use-after-free をやってしまった場合に、元のデータが無くなっていると気付きやすいためです。
- [5]メモリ確保の際には、前後にマージンを付けて確保します。そのマージン部分は 0xa5 で埋めておきます。これは「カナリア」と呼ばれるテクニックで、例えばプログラムがバッファオーバーランなどを起こすと、このカナリアを壊してしまいます。ライブラリはメモリの解放の際にはカナリアを確認しますが、もし壊されていたらさらにその外側にあるヒープメモリチェーンも壊れている可能性が高くて続行不能なので、エラー終了します。このバグを見落とすと追跡困難なバグになりやすいので、これは結構有用です。
- [6]確保したメモリの一覧がいつでも表示できるように、情報を貯めます。これで開放し忘れがないか確認できます。この情報は確保したメモリブロックのそばには置きません。なぜならカナリアと一緒に壊される可能性があるからです。もっと別の安全そうな場所に置くことにします。
(3) a_ というマクロ
- acl4v2 ライブラリでは a_ という引数がちょくちょく現れます。これは「 __FILE__, __LINE__, 」にdefineされていて、a_malloc()などがソースコードの何行目から呼び出されたのかを伝えるための仕組みです。できるだけ目立たないようにするためにこうなっています。
- ちなみに a_DbgLv=0 のときは、 a_ は「」になります。つまり消えます。だって余計な引数なんて渡して処理速度が落ちたら嫌ですよね?だからきれいに消えるのです。
- なぜ a_malloc() や a_free() で a_ を渡す必要があるのか説明します。まず a_free() のほうは簡単です。 a_free() でカナリア破壊検出などのエラーがあったときに、それはどの a_free() を呼んだときに気づいたのかわからないと、すごく不便だからです。ああ、〇〇行目の a_free() でこけたかーってわかればたいていすぐに直せます。
- 次に a_malloc() のほうです。 a_malloc() はメモリ不足以外では失敗しません。だからエラー個所を特定する必要なんて、ほぼないのです。しかもメモリ不足って、別にどこの行で起きたとか言われてもあまり参考になりません。メモリが尽きたタイミングでどこでも起きますし、起きてしまったら終了する以外にできることはありません。まあ例外として、サイズ指定を間違えたバグなのであれば、エラー行が分かることが役に立ちますが。
- じゃあなぜ a_ をとるのかと言えば、それは確保したメモリブロックに名前を付けるためです。〇〇行目の a_malloc() で確保したサイズ〇〇バイトのメモリってわかると、ああこれのことかーって気づけるんです。そうでないと、メモリアドレスとか言われても、いや、それってどのメモリのことですか?ってなるんです。
こめんと欄